Skip to main content

atmos/os_lib/js/interp/
dom_props.rs

1// 分割: interp.rs の `impl Interp` から機械的に移動(2026-07-16 リファクタ フェーズ5)。
2// ロジック不変。メソッド可視性のみ pub(crate) へ昇格(別モジュールの impl Interp
3// からの self 呼び出しのため)。impl ブロックは型 Interp に自動で結合する。
4use super::*;
5use crate::os_lib::js::builtins::this_dom_idx;
6
7impl Interp {
8    pub(crate) fn dom_get_property(&mut self, disp: DomDisp, key: &str) -> Value {
9        match disp {
10            DomDisp::Element(idx) => self.dom_element_get(idx, key),
11            DomDisp::Host(tag) => {
12                if let Some(rest) = tag.strip_prefix("style:") {
13                    match key {
14                        "setProperty" => {
15                            return Value::Object(Obj::native(
16                                "style.setProperty",
17                                super::super::builtins::dom_style_set_property,
18                            ))
19                        }
20                        "getPropertyValue" => {
21                            return Value::Object(Obj::native(
22                                "style.getPropertyValue",
23                                super::super::builtins::dom_style_get_property_value,
24                            ))
25                        }
26                        "removeProperty" => {
27                            return Value::Object(Obj::native(
28                                "style.removeProperty",
29                                super::super::builtins::dom_style_remove_property,
30                            ))
31                        }
32                        // `style.getPropertyPriority(name)`(丸ごと未対応
33                        // だった。2026-07-16 発見)。
34                        "getPropertyPriority" => {
35                            return Value::Object(Obj::native(
36                                "style.getPropertyPriority",
37                                super::super::builtins::dom_style_get_property_priority,
38                            ))
39                        }
40                        _ => {}
41                    }
42                    let idx: usize = rest.parse().unwrap_or(usize::MAX);
43                    // `element.style.cssText`(全プロパティを1つの文字列で読み書きする
44                    // 定番パターン)が丸ごと未対応で、通常の CSS プロパティ名として扱う
45                    // 汎用経路に落ちて常に空文字列になっていた。
46                    if key == "cssText" {
47                        return Value::str(self.dom.borrow().get_css_text(idx));
48                    }
49                    // `style.length`/`.item(index)`(丸ごと未対応だった。
50                    // `for (let i=0;i<style.length;i++) style.item(i)`という
51                    // 定番の反復イディオム。2026-07-17 発見)。専用の内部
52                    // 列挙APIは無いため、既存の`get_css_text`が返す直列化
53                    // 済み文字列(`"color: red; font-size: 12px"`)を`;`で
54                    // 分割し直す簡略実装(プロパティ名はkebab-caseのまま)。
55                    let style_names = || {
56                        self.dom
57                            .borrow()
58                            .get_css_text(idx)
59                            .split(';')
60                            .filter_map(|d| d.split_once(':').map(|(k, _)| k.trim().to_string()))
61                            .filter(|k| !k.is_empty())
62                            .collect::<alloc::vec::Vec<_>>()
63                    };
64                    if key == "length" {
65                        return Value::Number(style_names().len() as f64);
66                    }
67                    if key == "item" {
68                        return Value::Object(Obj::native("item", super::super::builtins::dom_style_item));
69                    }
70                    // `style.cssFloat`(`float`プロパティのレガシー別名。
71                    // `float`はかつて一部のJS実装で予約語扱いされていた
72                    // 名残の互換名。丸ごと未対応だった。2026-07-17 発見)。
73                    let key = if key == "cssFloat" { "float" } else { key };
74                    return Value::str(self.dom.borrow().get_style(idx, key));
75                }
76                if let Some(rest) = tag.strip_prefix("stylesheet:") {
77                    let idx: usize = rest.parse().unwrap_or(usize::MAX);
78                    match key {
79                        "ownerNode" => return Value::Object(Obj::dom(idx)),
80                        "insertRule" => {
81                            return Value::Object(Obj::native(
82                                "StyleSheet.insertRule",
83                                super::super::builtins::dom_stylesheet_insert_rule,
84                            ))
85                        }
86                        "deleteRule" => {
87                            return Value::Object(Obj::native(
88                                "StyleSheet.deleteRule",
89                                super::super::builtins::dom_stylesheet_delete_rule,
90                            ))
91                        }
92                        "cssRules" => {
93                            let text = self.dom.borrow().get_text_content(idx);
94                            let rules = super::super::builtins::split_css_rules(&text);
95                            let rule_objs: alloc::vec::Vec<Value> = rules
96                                .into_iter()
97                                .enumerate()
98                                .map(|(r_idx, rule_text)| {
99                                    let r_obj = Obj::host(&format!("cssrule:{}:{}", idx, r_idx));
100                                    r_obj.borrow_mut().props.insert("cssText".into(), Value::str(rule_text.clone()));
101                                    let selector = rule_text.split('{').next().unwrap_or("").trim().to_string();
102                                    r_obj.borrow_mut().props.insert("selectorText".into(), Value::str(selector));
103                                    Value::Object(r_obj)
104                                })
105                                .collect();
106                            return Value::Object(Obj::array(rule_objs));
107                        }
108                        _ => {}
109                    }
110                }
111                if let Some(rest) = tag.strip_prefix("classList:") {
112                    let cidx: usize = rest.parse().unwrap_or(usize::MAX);
113                    // メソッドはネイティブで返す(this から idx を取る)。
114                    return match key {
115                        "add" => Value::Object(Obj::native(
116                            "classList.add",
117                            super::super::builtins::dom_classlist_add,
118                        )),
119                        "remove" => Value::Object(Obj::native(
120                            "classList.remove",
121                            super::super::builtins::dom_classlist_remove,
122                        )),
123                        "toggle" => Value::Object(Obj::native(
124                            "classList.toggle",
125                            super::super::builtins::dom_classlist_toggle,
126                        )),
127                        "contains" => Value::Object(Obj::native(
128                            "classList.contains",
129                            super::super::builtins::dom_classlist_contains,
130                        )),
131                        "replace" => Value::Object(Obj::native(
132                            "classList.replace",
133                            super::super::builtins::dom_classlist_replace,
134                        )),
135                        "item" => Value::Object(Obj::native(
136                            "classList.item",
137                            super::super::builtins::dom_classlist_item,
138                        )),
139                        "toString" => Value::Object(Obj::native(
140                            "classList.toString",
141                            super::super::builtins::dom_classlist_to_string,
142                        )),
143                        "forEach" => Value::Object(Obj::native(
144                            "classList.forEach",
145                            super::super::builtins::dom_classlist_for_each,
146                        )),
147                        "entries" => Value::Object(Obj::native(
148                            "classList.entries",
149                            super::super::builtins::dom_classlist_entries,
150                        )),
151                        "keys" => Value::Object(Obj::native(
152                            "classList.keys",
153                            super::super::builtins::dom_classlist_keys,
154                        )),
155                        "values" => Value::Object(Obj::native(
156                            "classList.values",
157                            super::super::builtins::dom_classlist_values,
158                        )),
159                        "supports" => Value::Object(Obj::native(
160                            "classList.supports",
161                            super::super::builtins::dom_classlist_supports,
162                        )),
163                        "length" => Value::Number(
164                            self.dom
165                                .borrow()
166                                .nodes
167                                .get(cidx)
168                                .map(|n| n.classes.len())
169                                .unwrap_or(0) as f64,
170                        ),
171                        "value" => Value::str(
172                            self.dom
173                                .borrow()
174                                .nodes
175                                .get(cidx)
176                                .map(|n| n.classes.join(" "))
177                                .unwrap_or_default(),
178                        ),
179                        _ => Value::Undefined,
180                    };
181                }
182                if let Some(rest) = tag.strip_prefix("dataset:") {
183                    let idx: usize = rest.parse().unwrap_or(usize::MAX);
184                    let attr = camel_to_data_attr(key);
185                    return match self.dom.borrow().get_attr(idx, &attr) {
186                        Some(v) => Value::str(v),
187                        None => Value::Undefined,
188                    };
189                }
190                if let Some(rest) = tag.strip_prefix("computed:") {
191                    if key == "getPropertyValue" {
192                        return Value::Object(Obj::native(
193                            "getPropertyValue",
194                            super::super::builtins::computed_get_property_value,
195                        ));
196                    }
197                    let idx: usize = rest.parse().unwrap_or(usize::MAX);
198                    // camelCase(backgroundColor)→ kebab(background-color)。既に kebab ならそのまま。
199                    let prop = camel_to_kebab(key);
200                    return Value::str(self.dom.borrow().get_computed(idx, &prop));
201                }
202                Value::Undefined
203            }
204        }
205    }
206
207    /// 要素の組込みメンバだけを引く(expando・カスタム要素のクラスを見ない)。
208    /// `Element.prototype.setAttribute` 等の転送用ネイティブが使う。
209    pub(crate) fn dom_builtin_get(&mut self, idx: usize, key: &str) -> Value {
210        self.dom_builtin_lookup = true;
211        let v = self.dom_element_get(idx, key);
212        self.dom_builtin_lookup = false;
213        v
214    }
215
216    /// 要素の組込みプロパティに書く(クラスの setter を経由しない)。
217    pub(crate) fn dom_builtin_set(&mut self, idx: usize, key: &str, val: Value) {
218        self.dom_set_property(DomDisp::Element(idx), key, val);
219        self.process_ce_reactions();
220    }
221
222    /// 要素の原型連鎖に JS が定義した上書き(ネイティブでない関数・getter)があれば返す。
223    /// 【2026-10-20】YouTube は `Node.prototype.appendChild` 等を JS で包み、挿入時に
224    /// monomer 部品の接続処理を呼ぶ(`usePatchedLifecycles`)。組込みが先だと包みが効かず、
225    /// ytd-page-manager が接続されないまま視聴ページが描かれなかった。
226    fn dom_proto_override(&mut self, idx: usize, key: &str) -> Option<Value> {
227        let mut cur = self.get_dom_node_proto(idx);
228        let mut guard = 0;
229        let is_native = |v: &Value| matches!(v, Value::Object(o) if matches!(o.borrow().kind, ObjKind::Native { .. }));
230        while let Some(p) = cur {
231            let acc = p.borrow().accessors.get(key).map(|a| a.get.clone());
232            if let Some(g) = acc {
233                return match g {
234                    Some(g) if !is_native(&g) => {
235                        Some(self.call_value(&g, Value::Object(Obj::dom(idx)), &[]).unwrap_or(Value::Undefined))
236                    }
237                    _ => None,
238                };
239            }
240            if let Some(v) = p.borrow().props.get(key).cloned() {
241                return if is_native(&v) || !matches!(v, Value::Object(_)) { None } else { Some(v) };
242            }
243            guard += 1;
244            if guard > 64 {
245                break;
246            }
247            cur = p.borrow().proto.clone();
248        }
249        None
250    }
251
252    /// 昇格したカスタム要素のクラスの原型連鎖から `key` を引く(無ければ None)。
253    fn ce_chain_get(&mut self, idx: usize, key: &str) -> Option<Value> {
254        let mut cur = self.dom.borrow().node_protos.get(&idx).cloned();
255        let mut guard = 0;
256        while let Some(p) = cur {
257            let getter = p.borrow().accessors.get(key).map(|a| a.get.clone());
258            if let Some(g) = getter {
259                return Some(match g {
260                    Some(g) => self.call_value(&g, Value::Object(Obj::dom(idx)), &[]).unwrap_or(Value::Undefined),
261                    None => Value::Undefined,
262                });
263            }
264            if let Some(v) = p.borrow().props.get(key).cloned() {
265                return Some(v);
266            }
267            guard += 1;
268            if guard > 64 {
269                break;
270            }
271            cur = p.borrow().proto.clone();
272        }
273        None
274    }
275
276    pub(crate) fn dom_element_get(&mut self, idx: usize, key: &str) -> Value {
277        let builtin_only = core::mem::replace(&mut self.dom_builtin_lookup, false);
278        if !builtin_only {
279            // JS が付けた独自プロパティ(組込みの上書きも含む)が最優先。
280            if let Some(v) = self.dom.borrow().expandos.get(&idx).and_then(|m| m.get(key)) {
281                return v.clone();
282            }
283            // 昇格したカスタム要素は、クラス(とその先の Element.prototype 等)の定義が組込みより先。
284            // 【2026-10-20】Polymer(legacyOptimizations)は setAttribute/removeAttribute を
285            // prototype で上書きし、その中で disable-upgrade の反応を起こす。組込みが先だと
286            // YouTube の ytd-app が起動しなかった。
287            if self.dom.borrow().node_protos.contains_key(&idx) {
288                if let Some(v) = self.ce_chain_get(idx, key) {
289                    return v;
290                }
291            } else if let Some(v) = self.dom_proto_override(idx, key) {
292                return v;
293            }
294        }
295        match key {
296            "__proto__" => self.get_dom_node_proto(idx).map(Value::Object).unwrap_or(Value::Null),
297            // `node.baseURI`(DOM 標準。丸ごと未対応だった)。`<base>` タグ対応が
298            // 無いため、全ノードで `document.URL`/`location.href` と同一の値を
299            // 返す簡略実装。
300            "baseURI" => Value::str(super::super::builtins::location_href(&self.global).unwrap_or_default()),
301            // `element.attributes`(DOM 標準の `NamedNodeMap`。丸ごと未対応
302            // だった)。本物の `NamedNodeMap`(`getNamedItem`/名前でのブラケット
303            // アクセス等)は実装せず、`{name, value}` オブジェクトの配列という
304            // 簡略表現とする(`for...of`/`.length`/添字アクセスという最頻出の
305            // 利用パターンはこれで満たせる)。
306            "attributes" => {
307                let dom = self.dom.borrow();
308                let named: Vec<(String, Value)> = dom
309                    .attr_names(idx)
310                    .into_iter()
311                    .map(|name| {
312                        let value = dom.get_attr(idx, &name).unwrap_or_default();
313                        let o = Obj::plain();
314                        o.borrow_mut().props.insert("name".into(), Value::str(name.clone()));
315                        o.borrow_mut().props.insert("localName".into(), Value::str(name.clone()));
316                        o.borrow_mut().props.insert("value".into(), Value::str(value));
317                        (name, Value::Object(o))
318                    })
319                    .collect();
320                // NamedNodeMap の主な形(`getNamedItem`/`item`/名前での参照)も持たせる。
321                // 【2026-10-20】YouTube(Polymer の setUpProps)が `attributes.getNamedItem(…)` を使う。
322                let arr = Obj::array(named.iter().map(|(_, v)| v.clone()).collect());
323                {
324                    let mut b = arr.borrow_mut();
325                    for (n, v) in &named {
326                        b.props.insert(n.clone(), v.clone());
327                    }
328                    b.props.insert("getNamedItem".into(), Value::Object(Obj::native("getNamedItem", super::super::builtins::named_node_map_get)));
329                    b.props.insert("item".into(), Value::Object(Obj::native("item", super::super::builtins::named_node_map_item)));
330                }
331                Value::Object(arr)
332            }
333            "textContent" | "innerText" | "outerText" => {
334                Value::str(self.dom.borrow().get_text_content(idx))
335            }
336            // `option.text`(`<option>` 専用の `textContent` の別名。`new Option(...)`/
337            // `option.text = '...'` という定番パターン)が丸ごと未対応だった。
338            "text" => Value::str(self.dom.borrow().get_text_content(idx)),
339            // `innerHTML` は以前 `textContent` と同じ実装を共有しており、子要素の
340            // マークアップ自体(タグ)が失われ、全テキストを連結しただけの値になる
341            // バグだった。`outerHTML`(丸ごと未対応だった。自身のタグを含む)と
342            // 合わせて新設の DOM 側シリアライザで実装する。
343            "innerHTML" => {
344                let is_template = self.dom.borrow().nodes.get(idx).map(|n| n.tag == "template").unwrap_or(false);
345                if is_template {
346                    let frag_val = self.dom_element_get(idx, "content");
347                    if let Some(fi) = this_dom_idx(&frag_val) {
348                        Value::str(self.dom.borrow().get_inner_html(fi))
349                    } else {
350                        Value::str(self.dom.borrow().get_inner_html(idx))
351                    }
352                } else {
353                    Value::str(self.dom.borrow().get_inner_html(idx))
354                }
355            }
356            "outerHTML" => {
357                let is_template = self.dom.borrow().nodes.get(idx).map(|n| n.tag == "template").unwrap_or(false);
358                if is_template {
359                    Value::str("<template></template>")
360                } else {
361                    Value::str(self.dom.borrow().get_outer_html(idx))
362                }
363            }
364            "id" => Value::str(self.dom.borrow().get_attr(idx, "id").unwrap_or_default()),
365            "attachShadow" => {
366                Value::Object(Obj::native(
367                    "attachShadow",
368                    super::super::builtins::dom_attach_shadow,
369                ))
370            }
371            "shadowRoot" => {
372                if let Some(root_idx) = self.dom.borrow().get_shadow_root(idx) {
373                    Value::Object(Obj::dom(root_idx))
374                } else {
375                    Value::Null
376                }
377            }
378            "className" => Value::str(self.dom.borrow().get_attr(idx, "class").unwrap_or_default()),
379            "tagName" => {
380                let t = self
381                    .dom
382                    .borrow()
383                    .nodes
384                    .get(idx)
385                    .map(|n| {
386                        if n.attrs.contains_key("_xml") {
387                            n.tag.clone()
388                        } else {
389                            n.tag.to_uppercase()
390                        }
391                    })
392                    .unwrap_or_default();
393                Value::str(t)
394            }
395            // `element.constructor`(`instanceof HTMLDivElement`等は`get_dom_node_proto`
396            // 経由のタグ→プロトタイプ解決で既に対応済みだったが、`.constructor`
397            // プロパティ自体はDOM要素の`props`/`proto`連鎖には無く未対応だった。
398            // `instanceof`と同じタグ→プロトタイプ解決を再利用し、そのプロトタイプが
399            // 持つ`constructor`データプロパティ(`make_dom_cls`で設定済み)を返す。
400            // 2026-07-18 発見・実装)。
401            "constructor" => {
402                match self.get_dom_node_proto(idx).and_then(|p| p.borrow().props.get("constructor").cloned()) {
403                    Some(v) => v,
404                    None => Value::Undefined,
405                }
406            }
407            // `element.namespaceURI`/`.localName`/`.prefix`(丸ごと未対応
408            // だった)。この処理系は`*AttributeNS`系/`createElementNS`と
409            // 同じくXML名前空間を一切モデル化していないため、`namespaceURI`
410            // は常にHTML名前空間の定数、`localName`はタグ名そのもの
411            // (プレフィックス分離が無いため`tagName`の小文字版と同一)、
412            // `prefix`は常に`null`という誠実な簡略実装とする。テキスト
413            // ノードは仕様どおり全て`null`。2026-07-18 発見・実装)。
414            "namespaceURI" => {
415                let is_text = self.dom.borrow().nodes.get(idx).map(|n| n.is_text).unwrap_or(false);
416                if is_text {
417                    Value::Null
418                } else {
419                    Value::str("http://www.w3.org/1999/xhtml")
420                }
421            }
422            "localName" => {
423                let dom = self.dom.borrow();
424                match dom.nodes.get(idx) {
425                    Some(n) if !n.is_text => Value::str(n.tag.clone()),
426                    _ => Value::Null,
427                }
428            }
429            "prefix" => Value::Null,
430            // `<select>` は仕様上 `value` 属性を自身に持たず、選択状態は子の
431            // `<option selected>` が持つため、汎用の属性直結 getter では常に空文字列に
432            // なるバグだった。`selectedIndex` も丸ごと未対応だった。
433            // `<textarea>` も同種のバグ: 初期値はテキストノードの内容(`value` 属性では
434            // ない)で表現されるため、JS からまだ書き込んでいない `textarea.value` の
435            // 読み出しが常に空文字列になっていた(`collect_form_data` は `value` 属性
436            // 優先・無ければ `initial_text` というフォールバックを既に実装済みだったが、
437            // JS プロパティの getter 側にはこのフォールバックが欠けていた)。
438            // `li.value`(順序リストの番号を明示的に上書きする`long`型IDL
439            // プロパティ。仕様上、`value`属性が無いか不正値の場合の既定値は
440            // `0`。実際の表示番号への反映機構は無いため属性値の反映のみ
441            // 行う簡略実装だが、プロパティ自体が丸ごと未対応だった。
442            // 汎用の`value`アーム(次項)に落ちると数値ではなく文字列に
443            // なってしまうため、先にタグ限定で判定する。2026-07-17
444            // 発見・実装)。
445            "value" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "li").unwrap_or(false) => {
446                let v = self
447                    .dom
448                    .borrow()
449                    .get_attr(idx, "value")
450                    .and_then(|s| s.trim().parse::<i64>().ok())
451                    .unwrap_or(0);
452                Value::Number(v as f64)
453            }
454            "value" => {
455                let dom = self.dom.borrow();
456                let tag = dom.nodes.get(idx).map(|n| n.tag.as_str()).unwrap_or("");
457                if tag == "select" {
458                    Value::str(dom.select_effective_value(idx))
459                } else if tag == "textarea" {
460                    Value::str(
461                        dom.get_attr(idx, "_live_value")
462                            .or_else(|| dom.get_attr(idx, "value"))
463                            .unwrap_or_else(|| dom.nodes.get(idx).map(|n| n.initial_text.clone()).unwrap_or_default()),
464                    )
465                } else if tag == "output" {
466                    // `output.value`/`.defaultValue`が丸ごと未対応だった。
467                    // `<output>`は`value`content属性を持たず、仕様上`.value`
468                    // はJSから明示的に代入されていなければ`.textContent`を
469                    // 返す(`textarea.value`と同じ「未書込み時は表示内容へ
470                    // フォールバック」パターン)。`textarea`は`initial_text`
471                    // (パース時点の内容)にフォールバックするが、`output`は
472                    // 現在の`textContent`にフォールバックする点が異なる
473                    // (`defaultValue`側で仕様上のより厳密な区別が必要だが、
474                    // 今回はスコープ外)。2026-07-18 発見・実装)。
475                    Value::str(dom.get_attr(idx, "value").unwrap_or_else(|| dom.get_text_content(idx)))
476                } else if tag == "progress" || tag == "meter" {
477                    // `<progress>`/`<meter>` は仕様上 `.value` が数値プロパティ(丸ごと
478                    // 未対応だった。汎用の属性直結分岐だと常に文字列 `""`/属性値の
479                    // 生文字列になってしまい `progress.value` が数値として使えなかった)。
480                    Value::Number(
481                        dom.get_attr(idx, "value").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(0.0),
482                    )
483                } else if tag == "input" && dom.get_attr(idx, "type").as_deref() == Some("file") {
484                    Value::str("")
485                } else if tag == "input"
486                    && matches!(dom.get_attr(idx, "type").as_deref(), Some("checkbox") | Some("radio"))
487                {
488                    let raw_val = dom.get_attr(idx, "_live_value").or_else(|| dom.get_attr(idx, "value"));
489                    Value::str(raw_val.unwrap_or_else(|| String::from("on")))
490                } else if tag == "input"
491                    && dom.get_attr(idx, "type").as_deref() == Some("color")
492                {
493                    let is_valid_hex_color = |s: &str| {
494                        s.len() == 7
495                            && s.starts_with('#')
496                            && s.chars().skip(1).all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
497                    };
498                    let raw_val = dom.get_attr(idx, "_live_value").or_else(|| dom.get_attr(idx, "value"));
499                    match raw_val {
500                        Some(v) if is_valid_hex_color(&v) => Value::str(v),
501                        _ => Value::str("#000000"),
502                    }
503                } else if tag == "input" && dom.get_attr(idx, "type").as_deref() == Some("range") {
504                    let min = dom.get_attr(idx, "min").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(0.0);
505                    let max = dom.get_attr(idx, "max").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(100.0);
506                    let default = if max < min { min } else { min + (max - min) / 2.0 };
507                    let raw_val = dom.get_attr(idx, "_live_value").or_else(|| dom.get_attr(idx, "value"));
508                    let v = raw_val
509                        .and_then(|s| s.trim().parse::<f64>().ok())
510                        .unwrap_or(default);
511                    let clamped = if max < min {
512                        min
513                    } else {
514                        v.clamp(min, max)
515                    };
516                    Value::str(Value::Number(clamped).to_js_string())
517                } else if tag == "input"
518                    && matches!(
519                        dom.get_attr(idx, "type").as_deref(),
520                        Some("date") | Some("month") | Some("datetime-local")
521                    )
522                {
523                    let ty = dom.get_attr(idx, "type").unwrap_or_default();
524                    let raw_val = dom.get_attr(idx, "_live_value").or_else(|| dom.get_attr(idx, "value"));
525                    match raw_val {
526                        Some(v) if super::super::builtins::input_value_as_date_ms(&ty, &v).is_some() => {
527                            Value::str(v)
528                        }
529                        _ => Value::str(""),
530                    }
531                } else {
532                    let raw_val = dom.get_attr(idx, "_live_value").or_else(|| dom.get_attr(idx, "value"));
533                    Value::str(raw_val.unwrap_or_default())
534                }
535            }
536            // `input`/`textarea.defaultValue`(`value`属性/初期テキストを
537            // 反映するIDLプロパティ。`.value`と同じ「未書込み時のフォール
538            // バック」パターン)と`input.defaultChecked`/`option.
539            // defaultSelected`(`checked`/`selected`属性の有無を反映する
540            // ブール型)が丸ごと未対応だった。フォームリセット(`.reset()`
541            // は仕様上これらの既定値へ戻す)や、React等のフレームワークが
542            // 制御/非制御入力の判別に使う定番プロパティ。2026-07-17
543            // 発見・実装)。
544            "defaultValue" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "textarea").unwrap_or(false) => {
545                let dom = self.dom.borrow();
546                Value::str(
547                    dom.get_attr(idx, "value")
548                        .unwrap_or_else(|| dom.nodes.get(idx).map(|n| n.initial_text.clone()).unwrap_or_default()),
549                )
550            }
551            "defaultValue" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "input").unwrap_or(false) => {
552                Value::str(self.dom.borrow().get_attr(idx, "value").unwrap_or_default())
553            }
554            // `output.defaultValue`(`output.value`と対になる既定値追跡
555            // プロパティ。丸ごと未対応だった。`initial_text`(パース時点の
556            // 内容)にフォールバックする点は`textarea.defaultValue`と同じ。
557            // 2026-07-18 発見・実装)。
558            "defaultValue" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "output").unwrap_or(false) => {
559                let dom = self.dom.borrow();
560                Value::str(
561                    dom.get_attr(idx, "value")
562                        .unwrap_or_else(|| dom.nodes.get(idx).map(|n| n.initial_text.clone()).unwrap_or_default()),
563                )
564            }
565            "defaultChecked" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "input").unwrap_or(false) => {
566                Value::Bool(self.dom.borrow().has_attr(idx, "checked"))
567            }
568            "defaultSelected" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "option").unwrap_or(false) => {
569                Value::Bool(self.dom.borrow().has_attr(idx, "selected"))
570            }
571            // `input`/`textarea.selectionStart`/`.selectionEnd`/
572            // `.selectionDirection`(丸ごと未対応だった。2026-07-17 発見)。
573            // この処理系には実際のキャレット/テキスト選択機構が無いため、
574            // 内部専用属性(`_selection_start`等)へ書き込み値をそのまま
575            // 保存するだけの状態追跡のみの簡略実装(`checkbox.indeterminate`
576            // と同方針)。未設定時は仕様どおり`0`/`"none"`。
577            "selectionStart" | "selectionEnd"
578                if self
579                    .dom
580                    .borrow()
581                    .nodes
582                    .get(idx)
583                    .map(|n| n.tag == "input" || n.tag == "textarea")
584                    .unwrap_or(false) =>
585            {
586                let attr = if key == "selectionStart" { "_selection_start" } else { "_selection_end" };
587                Value::Number(self.dom.borrow().get_attr(idx, attr).and_then(|v| v.parse::<f64>().ok()).unwrap_or(0.0))
588            }
589            "selectionDirection"
590                if self
591                    .dom
592                    .borrow()
593                    .nodes
594                    .get(idx)
595                    .map(|n| n.tag == "input" || n.tag == "textarea")
596                    .unwrap_or(false) =>
597            {
598                Value::str(self.dom.borrow().get_attr(idx, "_selection_direction").unwrap_or_else(|| "none".into()))
599            }
600            // `textarea.textLength`(仕様上`HTMLTextAreaElement`限定。`.value`
601            // の文字数を毎回`.length`で数える手間を省くIDL属性。丸ごと
602            // 未対応だった)。
603            "textLength" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "textarea").unwrap_or(false) => {
604                let dom = self.dom.borrow();
605                let v = dom
606                    .get_attr(idx, "_live_value")
607                    .or_else(|| dom.get_attr(idx, "value"))
608                    .unwrap_or_else(|| dom.nodes.get(idx).map(|n| n.initial_text.clone()).unwrap_or_default());
609                Value::Number(v.chars().count() as f64)
610            }
611            // `textarea.wrap`(`"soft"`/`"hard"`の列挙属性。既知値以外・
612            // 欠落時は仕様上の既定値`"soft"`にフォールバックする必要が
613            // あるが、丸ごと未対応だった。`fieldset.type`等と同じバグ
614            // パターン。2026-07-17 発見・実装)。
615            "wrap" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "textarea").unwrap_or(false) => {
616                let v = self.dom.borrow().get_attr(idx, "wrap").map(|s| s.to_lowercase());
617                match v.as_deref() {
618                    Some("hard") => Value::str("hard"),
619                    _ => Value::str("soft"),
620                }
621            }
622            // `setSelectionRange(start, end, direction?)`/`setRangeText
623            // (replacement, start?, end?, selectMode?)`(丸ごと未対応
624            // だった。`selectionStart`等と対になる操作メソッド。
625            // 2026-07-17 発見・実装)。
626            "setSelectionRange"
627                if self
628                    .dom
629                    .borrow()
630                    .nodes
631                    .get(idx)
632                    .map(|n| n.tag == "input" || n.tag == "textarea")
633                    .unwrap_or(false) =>
634            {
635                Value::Object(Obj::native("setSelectionRange", super::super::builtins::dom_set_selection_range))
636            }
637            "setRangeText"
638                if self
639                    .dom
640                    .borrow()
641                    .nodes
642                    .get(idx)
643                    .map(|n| n.tag == "input" || n.tag == "textarea")
644                    .unwrap_or(false) =>
645            {
646                Value::Object(Obj::native("setRangeText", super::super::builtins::dom_set_range_text))
647            }
648            // `input.valueAsNumber`(HTML5。`type="number"`/`"range"` の `.value`
649            // 文字列を毎回 `Number()` で変換する定番の手間を省く IDL 属性。丸ごと
650            // 未対応だった。対象外の `type` や未入力/不正値では仕様どおり `NaN`)。
651            "valueAsNumber" => {
652                let dom = self.dom.borrow();
653                let ty = dom.get_attr(idx, "type").unwrap_or_default();
654                if ty == "number" || ty == "range" {
655                    let raw_val = dom.get_attr(idx, "_live_value").or_else(|| dom.get_attr(idx, "value"));
656                    Value::Number(
657                        raw_val
658                            .and_then(|s| s.trim().parse::<f64>().ok())
659                            .unwrap_or(f64::NAN),
660                    )
661                } else {
662                    Value::Number(f64::NAN)
663                }
664            }
665            // `input.valueAsDate`(HTML5。`type="date"`/`"month"` の `.value` 文字列を
666            // `Date` オブジェクトとして読み書きする IDL 属性。丸ごと未対応だった。
667            // 対象外の `type` や未入力/不正値では仕様どおり `null`(`valueAsNumber`
668            // の `NaN` とは異なる)。
669            "valueAsDate" => {
670                let dom = self.dom.borrow();
671                let ty = dom.get_attr(idx, "type").unwrap_or_default();
672                let value = dom.get_attr(idx, "_live_value")
673                    .or_else(|| dom.get_attr(idx, "value"))
674                    .unwrap_or_default();
675                match super::super::builtins::input_value_as_date_ms(&ty, &value) {
676                    Some(ms) => Value::Object(Obj::date_obj(ms)),
677                    None => Value::Null,
678                }
679            }
680            // `<progress>`/`<meter>` の `max`/`min`(`<meter>` のみ)。既定値は仕様どおり
681            // `max`=1、`min`=0。
682            "max"
683                if self
684                    .dom
685                    .borrow()
686                    .nodes
687                    .get(idx)
688                    .map(|n| n.tag == "progress" || n.tag == "meter")
689                    .unwrap_or(false) =>
690            {
691                Value::Number(
692                    self.dom
693                        .borrow()
694                        .get_attr(idx, "max")
695                        .and_then(|s| s.trim().parse::<f64>().ok())
696                        .unwrap_or(1.0),
697                )
698            }
699            "min"
700                if self
701                    .dom
702                    .borrow()
703                    .nodes
704                    .get(idx)
705                    .map(|n| n.tag == "meter")
706                    .unwrap_or(false) =>
707            {
708                Value::Number(
709                    self.dom
710                        .borrow()
711                        .get_attr(idx, "min")
712                        .and_then(|s| s.trim().parse::<f64>().ok())
713                        .unwrap_or(0.0),
714                )
715            }
716            // `progress.position`(廃止予定だが仕様に残る読み取り専用IDL
717            // プロパティ。`value`属性が無い不確定状態なら`-1`、それ以外は
718            // `value/max`を返す。丸ごと未対応だった。2026-07-18 発見・
719            // 実装)。
720            "position"
721                if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "progress").unwrap_or(false) =>
722            {
723                let dom = self.dom.borrow();
724                match dom.get_attr(idx, "value").and_then(|s| s.trim().parse::<f64>().ok()) {
725                    None => Value::Number(-1.0),
726                    Some(v) => {
727                        let max = dom.get_attr(idx, "max").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(1.0);
728                        let max = if max > 0.0 { max } else { 1.0 };
729                        Value::Number((v / max).clamp(0.0, 1.0))
730                    }
731                }
732            }
733            // `meter.low`/`.high`/`.optimum`(`min`/`max`と同じく丸ごと
734            // 未対応だった。仕様上、`low`の既定値は`min`、`high`の既定値は
735            // `max`、`optimum`の既定値は`min`と`max`の中点。それぞれの
736            // 属性値が範囲外の場合も仕様どおりクランプする。2026-07-17
737            // 発見・実装)。
738            "low" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "meter").unwrap_or(false) => {
739                let dom = self.dom.borrow();
740                let min = dom.get_attr(idx, "min").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(0.0);
741                let max = dom.get_attr(idx, "max").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(1.0);
742                let low = dom.get_attr(idx, "low").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(min);
743                Value::Number(low.clamp(min, max))
744            }
745            "high" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "meter").unwrap_or(false) => {
746                let dom = self.dom.borrow();
747                let min = dom.get_attr(idx, "min").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(0.0);
748                let max = dom.get_attr(idx, "max").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(1.0);
749                let high = dom.get_attr(idx, "high").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(max);
750                Value::Number(high.clamp(min, max))
751            }
752            "optimum" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "meter").unwrap_or(false) => {
753                let dom = self.dom.borrow();
754                let min = dom.get_attr(idx, "min").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(0.0);
755                let max = dom.get_attr(idx, "max").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(1.0);
756                let optimum =
757                    dom.get_attr(idx, "optimum").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or((min + max) / 2.0);
758                Value::Number(optimum.clamp(min, max))
759            }
760            "selectedIndex" => Value::Number(self.dom.borrow().select_selected_index(idx) as f64),
761            // `select.add(element, before)`(`select.options.add`と同じ意味の
762            // 便利メソッド。丸ごと未対応だった。2026-07-18 発見・実装)。
763            "add" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "select").unwrap_or(false) => {
764                Value::Object(Obj::native("add", super::super::builtins::dom_select_add))
765            }
766            // `select.length`(`select.options.length`と同じ値を返すIDL属性。
767            // 丸ごと未対応だった)。`<select>`以外のタグでは意味を持たないため
768            // 対象外(他プロパティと同じ簡略方針)。
769            "length"
770                if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "select").unwrap_or(false) =>
771            {
772                Value::Number(self.dom.borrow().select_options(idx).len() as f64)
773            }
774            // `select.selectedOptions`(`<select multiple>` で選択中の option 一覧を得る
775            // 定番イディオム)が丸ごと未対応だった。`FormData` の multi-select 収集漏れ
776            // 修正と同じ根本原因(`selected` 属性の集約先が無かった)。
777            "selectedOptions" => {
778                let indices = self.dom.borrow().selected_option_indices(idx);
779                Value::Object(Obj::array(
780                    indices.into_iter().map(|i| Value::Object(Obj::dom(i))).collect(),
781                ))
782            }
783            // `select.options`(動的にドロップダウンを構築する定番イディオム
784            // `for (const o of select.options)`/`select.options[i]` で使われる)が
785            // `selectedOptions` と同じ理由で丸ごと未対応だった。
786            "options" => {
787                let indices = self.dom.borrow().select_options(idx);
788                let arr = Obj::array(
789                    indices.into_iter().map(|i| Value::Object(Obj::dom(i))).collect(),
790                );
791                // `select.options.add`/`.remove`/`.namedItem`(`HTMLOptionsCollection`。
792                // 丸ごと未対応だった。`_select_idx` は元の `<select>` を辿るための
793                // 隠しプロパティ)。
794                {
795                    let mut b = arr.borrow_mut();
796                    b.props.insert("_select_idx".into(), Value::Number(idx as f64));
797                    b.props.insert(
798                        "item".into(),
799                        Value::Object(Obj::native("options.item", super::super::builtins::html_collection_item)),
800                    );
801                    b.props.insert(
802                        "add".into(),
803                        Value::Object(Obj::native("options.add", super::super::builtins::select_options_add)),
804                    );
805                    b.props.insert(
806                        "remove".into(),
807                        Value::Object(Obj::native("options.remove", super::super::builtins::select_options_remove)),
808                    );
809                    b.props.insert(
810                        "namedItem".into(),
811                        Value::Object(Obj::native(
812                            "options.namedItem",
813                            super::super::builtins::select_options_named_item,
814                        )),
815                    );
816                }
817                Value::Object(arr)
818            }
819            // `input.form`(自身が属する `<form>` への逆参照。`el.form.reset()`/
820            // `el.form.submit()` という定番イディオムで使われる)が丸ごと未対応
821            // だった。`associated_form`は祖先探索(`closest_tag`)に加え、
822            // `form="formId"`属性による明示的なフォーム外関連付け(HTML5仕様の
823            // 「form owner」)も見る。
824            "form" => match self.dom.borrow().associated_form(idx) {
825                Some(fi) => Value::Object(Obj::dom(fi)),
826                None => Value::Null,
827            },
828            // `label.control`(`labels` の逆方向。`<label>` から自身に関連付いた
829            // フォームコントロールを引く)が丸ごと未対応だった。`for` 属性による
830            // 明示的な関連付けを優先し、無ければ子孫の最初のフォームコントロールへ
831            // フォールバックする。
832            "control" => {
833                let dom = self.dom.borrow();
834                let is_label = dom.nodes.get(idx).map(|n| n.tag == "label").unwrap_or(false);
835                if !is_label {
836                    Value::Undefined
837                } else {
838                    let for_id = dom.get_attr(idx, "for");
839                    let by_for = for_id.filter(|f| !f.is_empty()).and_then(|f| {
840                        (0..dom.nodes.len())
841                            .find(|&i| !dom.nodes[i].is_text && dom.nodes[i].id == f)
842                    });
843                    let target = by_for.or_else(|| {
844                        dom.descendants_by_tags(idx, &["input", "select", "textarea", "button"])
845                            .into_iter()
846                            .next()
847                    });
848                    match target {
849                        Some(i) => Value::Object(Obj::dom(i)),
850                        None => Value::Null,
851                    }
852                }
853            }
854            // `input.labels`(自身に紐づく `<label>` 一覧。`<label for="id">` による
855            // 明示的な関連付けと、`<label>` の子孫に置く暗黙の関連付けの両方が対象)が
856            // 丸ごと未対応だった。フォームアクセシビリティの定番パターンで使われる。
857            "labels" => {
858                let dom = self.dom.borrow();
859                let control_id = dom.nodes.get(idx).map(|n| n.id.clone()).unwrap_or_default();
860                let mut labels: Vec<usize> = Vec::new();
861                for (i, n) in dom.nodes.iter().enumerate() {
862                    if n.is_text || n.tag != "label" {
863                        continue;
864                    }
865                    let matches_for = !control_id.is_empty()
866                        && dom.get_attr(i, "for").map(|f| f == control_id).unwrap_or(false);
867                    let is_ancestor = dom.is_ancestor_of(i, idx);
868                    if matches_for || is_ancestor {
869                        labels.push(i);
870                    }
871                }
872                Value::Object(Obj::array(
873                    labels.into_iter().map(|i| Value::Object(Obj::dom(i))).collect(),
874                ))
875            }
876            // `form.elements`(`form.elements[i]`/`for (const el of form.elements)` で
877            // 全フォームコントロールを走査する定番イディオム)が丸ごと未対応だった。
878            // `form_associated_controls`(`form.checkValidity()`/`FormData`収集と
879            // 共通の「子孫 + form="このformのid"属性による外部関連付け」判定)を
880            // 再利用する。以前は`descendants_by_tags`(子孫のみ)だったため、
881            // `<form>`の外に置かれた`<input form="...">`が`.elements`からも
882            // 漏れる兄弟ギャップだった(名前/id によるプロパティアクセスは
883            // 対象外の簡略実装のまま)。
884            // `fieldset.elements`(丸ごと未対応だった。`HTMLFieldSetElement`の
885            // 仕様上のIDLプロパティ。`form.elements`と異なり`form="id"`に
886            // よる木構造外の関連付けは対象外で、純粋なDOM子孫のみを対象と
887            // する。2026-07-17 発見・実装)。
888            "elements" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "fieldset").unwrap_or(false) => {
889                let fields = self.dom.borrow().descendants_by_tags(idx, &["input", "select", "textarea", "button"]);
890                super::super::builtins::make_html_collection(self, fields)
891            }
892            "elements" => {
893                let dom = self.dom.borrow();
894                let is_form = dom.nodes.get(idx).map(|n| n.tag == "form").unwrap_or(false);
895                if is_form {
896                    let fields =
897                        dom.form_associated_controls(idx, &["input", "select", "textarea", "button"]);
898                    drop(dom);
899                    super::super::builtins::make_html_collection(self, fields)
900                } else {
901                    Value::Undefined
902                }
903            }
904            // `form.length`(`form.elements.length`と同じ値を返すIDL属性の
905            // 数値ショートハンド。`select.length`の対になる`<form>`側だが
906            // 丸ごと未対応だった。`elements`と全く同じ`form_associated_controls`
907            // を再利用する)。
908            "length" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "form").unwrap_or(false) => {
909                Value::Number(
910                    self.dom
911                        .borrow()
912                        .form_associated_controls(idx, &["input", "select", "textarea", "button"])
913                        .len() as f64,
914                )
915            }
916            // `table.rows`/`tr.cells`(HTML5 Table DOM。丸ごと未対応だった。
917            // 2026-07-17 発見)。`form.elements` と同じ `descendants_by_tags`
918            // を再利用する簡略実装(`caption`/`colgroup`は対象外、`thead`/
919            // `tbody`/`tfoot`内の`tr`も含めて文書順で列挙する)。
920            // `HTMLTableSectionElement.rows`(`thead`/`tbody`/`tfoot`自身が
921            // 持つ、そのセクション内に限定した`rows`。丸ごと未対応だった。
922            // `table.rows`は実装済みなのに対になるこちらだけ抜けていた
923            // 兄弟ギャップ。2026-07-17 発見・実装)。
924            "rows"
925                if self
926                    .dom
927                    .borrow()
928                    .nodes
929                    .get(idx)
930                    .map(|n| matches!(n.tag.as_str(), "table" | "thead" | "tbody" | "tfoot"))
931                    .unwrap_or(false) =>
932            {
933                let rows = self.dom.borrow().descendants_by_tags(idx, &["tr"]);
934                Value::Object(Obj::array(rows.into_iter().map(|i| Value::Object(Obj::dom(i))).collect()))
935            }
936            "cells" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "tr").unwrap_or(false) => {
937                let cells = self.dom.borrow().descendants_by_tags(idx, &["td", "th"]);
938                Value::Object(Obj::array(cells.into_iter().map(|i| Value::Object(Obj::dom(i))).collect()))
939            }
940            // `table.tHead`/`.tFoot`/`.caption`(直接の子のみが対象。仕様上
941            // `thead`/`tfoot`/`caption`はテーブルの直下にしか置けないため、
942            // `rows`/`cells`の`descendants_by_tags`のような子孫検索ではなく
943            // 直接の子だけを見る。丸ごと未対応だった。2026-07-17 発見・実装)。
944            "tHead" | "tFoot" | "caption"
945                if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "table").unwrap_or(false) =>
946            {
947                let want = match key {
948                    "tHead" => "thead",
949                    "tFoot" => "tfoot",
950                    _ => "caption",
951                };
952                let dom = self.dom.borrow();
953                let found = dom.nodes[idx].children.iter().find(|&&c| dom.nodes[c].tag == want).copied();
954                match found {
955                    Some(i) => Value::Object(Obj::dom(i)),
956                    None => Value::Null,
957                }
958            }
959            // `table.tBodies`(直接の子`tbody`のみを集約するHTMLCollection)。
960            "tBodies" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "table").unwrap_or(false) => {
961                let dom = self.dom.borrow();
962                let bodies: alloc::vec::Vec<usize> =
963                    dom.nodes[idx].children.iter().filter(|&&c| dom.nodes[c].tag == "tbody").copied().collect();
964                Value::Object(Obj::array(bodies.into_iter().map(|i| Value::Object(Obj::dom(i))).collect()))
965            }
966            // `tr.rowIndex`(自身が属する`table`内の`rows`集合における位置)。
967            "rowIndex" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "tr").unwrap_or(false) => {
968                let dom = self.dom.borrow();
969                let mut cur = dom.nodes.get(idx).and_then(|n| n.parent);
970                let table = loop {
971                    match cur {
972                        Some(p) if dom.nodes.get(p).map(|n| n.tag == "table").unwrap_or(false) => break Some(p),
973                        Some(p) => cur = dom.nodes.get(p).and_then(|n| n.parent),
974                        None => break None,
975                    }
976                };
977                match table {
978                    Some(t) => {
979                        let rows = dom.descendants_by_tags(t, &["tr"]);
980                        Value::Number(rows.iter().position(|&r| r == idx).map(|p| p as f64).unwrap_or(-1.0))
981                    }
982                    None => Value::Number(-1.0),
983                }
984            }
985            // `tr.sectionRowIndex`(丸ごと未対応だった。`rowIndex`は実装済み
986            // なのに、対になるこちらだけ抜けていた兄弟ギャップ。2026-07-17
987            // 発見・実装)。`rowIndex`が「テーブル全体の`rows`集合における
988            // 位置」なのに対し、こちらは「直接の親(`thead`/`tbody`/
989            // `tfoot`、無ければ`table`自身)配下の`tr`集合における位置」。
990            // `thead`に2行・`tbody`に3行ある表で`tbody`側の2行目の
991            // `tr`なら、`rowIndex`は`3`(テーブル全体で4番目)だが
992            // `sectionRowIndex`は`1`(tbody内で2番目、0始まり)になる。
993            "sectionRowIndex" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "tr").unwrap_or(false) => {
994                let dom = self.dom.borrow();
995                match dom.nodes.get(idx).and_then(|n| n.parent) {
996                    Some(p) => {
997                        let rows = dom.descendants_by_tags(p, &["tr"]);
998                        Value::Number(rows.iter().position(|&r| r == idx).map(|i| i as f64).unwrap_or(-1.0))
999                    }
1000                    None => Value::Number(-1.0),
1001                }
1002            }
1003            // `cell.cellIndex`(自身が属する`tr`内の`cells`集合における位置)。
1004            "cellIndex" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "td" || n.tag == "th").unwrap_or(false) => {
1005                let dom = self.dom.borrow();
1006                match dom.nodes.get(idx).and_then(|n| n.parent) {
1007                    Some(p) => {
1008                        let cells = dom.descendants_by_tags(p, &["td", "th"]);
1009                        Value::Number(cells.iter().position(|&c| c == idx).map(|i| i as f64).unwrap_or(-1.0))
1010                    }
1011                    None => Value::Number(-1.0),
1012                }
1013            }
1014            // `td`/`th.colSpan`/`.rowSpan`(丸ごと未対応だった。動的に表を
1015            // 組み立てる際の定番プロパティ。仕様上どちらも欠落・不正値時の
1016            // 既定値は`1`で、`colSpan`は最大`1000`、`rowSpan`は最大`65534`
1017            // にクランプする(`rowSpan`は`0`が「セクション末尾まで」を表す
1018            // 特殊な有効値のため`colSpan`と異なり下限クランプは無い)。
1019            // 2026-07-17 発見・実装)。
1020            "colSpan" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "td" || n.tag == "th").unwrap_or(false) => {
1021                let v = self
1022                    .dom
1023                    .borrow()
1024                    .get_attr(idx, "colspan")
1025                    .and_then(|s| s.trim().parse::<i64>().ok())
1026                    .filter(|n| *n >= 1)
1027                    .unwrap_or(1);
1028                Value::Number(v.min(1000) as f64)
1029            }
1030            "rowSpan" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "td" || n.tag == "th").unwrap_or(false) => {
1031                let v = self
1032                    .dom
1033                    .borrow()
1034                    .get_attr(idx, "rowspan")
1035                    .and_then(|s| s.trim().parse::<i64>().ok())
1036                    .filter(|n| *n >= 0)
1037                    .unwrap_or(1);
1038                Value::Number(v.min(65534) as f64)
1039            }
1040            // `td`/`th.headers`(空白区切りID文字列の単純な属性反映)と
1041            // `th.scope`(`"row"`/`"col"`/`"rowgroup"`/`"colgroup"`の列挙
1042            // 属性。既知値以外・欠落時は仕様上の既定値`""`(`auto`状態)に
1043            // フォールバックする)、`th.abbr`(単純な文字列反映)が丸ごと
1044            // 未対応だった。`colSpan`/`.rowSpan`と同じ点検で発見。
1045            // 2026-07-17 発見・実装)。
1046            "headers" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "td" || n.tag == "th").unwrap_or(false) => {
1047                Value::str(self.dom.borrow().get_attr(idx, "headers").unwrap_or_default())
1048            }
1049            "scope" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "th").unwrap_or(false) => {
1050                let v = self.dom.borrow().get_attr(idx, "scope").map(|s| s.to_lowercase());
1051                match v.as_deref() {
1052                    Some("row") => Value::str("row"),
1053                    Some("col") => Value::str("col"),
1054                    Some("rowgroup") => Value::str("rowgroup"),
1055                    Some("colgroup") => Value::str("colgroup"),
1056                    _ => Value::str(""),
1057                }
1058            }
1059            "abbr" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "th").unwrap_or(false) => {
1060                Value::str(self.dom.borrow().get_attr(idx, "abbr").unwrap_or_default())
1061            }
1062            // `col`/`colgroup.span`(`colSpan`/`.rowSpan`と同じく丸ごと
1063            // 未対応だった。仕様上、欠落・不正値時の既定値は`1`で最大
1064            // `1000`にクランプする。2026-07-17 発見・実装)。
1065            "span" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "col" || n.tag == "colgroup").unwrap_or(false) => {
1066                let v = self
1067                    .dom
1068                    .borrow()
1069                    .get_attr(idx, "span")
1070                    .and_then(|s| s.trim().parse::<i64>().ok())
1071                    .filter(|n| *n >= 1)
1072                    .unwrap_or(1);
1073                Value::Number(v.min(1000) as f64)
1074            }
1075            // `table.insertRow(index?)`/`.deleteRow(index)`、`tr.insertCell
1076            // (index?)`/`.deleteCell(index)`(HTML5 Table DOM。丸ごと未対応
1077            // だった。`select.options.add`/`.remove` と同じ「動的に表を
1078            // 組み立てる」定番イディオム)。
1079            // `HTMLTableSectionElement.insertRow`/`.deleteRow`(`thead`/
1080            // `tbody`/`tfoot`自身が持つ、そのセクション内に限定した版。
1081            // 丸ごと未対応だった。`table.insertRow`は実装済みなのに対になる
1082            // こちらだけ抜けていた兄弟ギャップ。`dom_table_insert_row`/
1083            // `dom_table_delete_row`は`this`を起点にスコープする汎用実装の
1084            // ため、`<table>`用に書いた関数をそのまま再利用できる。
1085            // 2026-07-17 発見・実装)。
1086            "insertRow"
1087                if self
1088                    .dom
1089                    .borrow()
1090                    .nodes
1091                    .get(idx)
1092                    .map(|n| matches!(n.tag.as_str(), "table" | "thead" | "tbody" | "tfoot"))
1093                    .unwrap_or(false) =>
1094            {
1095                Value::Object(Obj::native("insertRow", super::super::builtins::dom_table_insert_row))
1096            }
1097            "deleteRow"
1098                if self
1099                    .dom
1100                    .borrow()
1101                    .nodes
1102                    .get(idx)
1103                    .map(|n| matches!(n.tag.as_str(), "table" | "thead" | "tbody" | "tfoot"))
1104                    .unwrap_or(false) =>
1105            {
1106                Value::Object(Obj::native("deleteRow", super::super::builtins::dom_table_delete_row))
1107            }
1108            "insertCell" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "tr").unwrap_or(false) => {
1109                Value::Object(Obj::native("insertCell", super::super::builtins::dom_table_insert_cell))
1110            }
1111            "deleteCell" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "tr").unwrap_or(false) => {
1112                Value::Object(Obj::native("deleteCell", super::super::builtins::dom_table_delete_cell))
1113            }
1114            // `table.createTHead()`/`.createTFoot()`/`.createCaption()`と
1115            // 対になる`delete*`(既存要素があれば返す/削除するだけの
1116            // 冪等な簡略実装。丸ごと未対応だった。2026-07-17 発見・実装)。
1117            "createTHead" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "table").unwrap_or(false) => {
1118                Value::Object(Obj::native("createTHead", super::super::builtins::dom_table_create_thead))
1119            }
1120            "createTFoot" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "table").unwrap_or(false) => {
1121                Value::Object(Obj::native("createTFoot", super::super::builtins::dom_table_create_tfoot))
1122            }
1123            "createCaption" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "table").unwrap_or(false) => {
1124                Value::Object(Obj::native("createCaption", super::super::builtins::dom_table_create_caption))
1125            }
1126            "deleteTHead" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "table").unwrap_or(false) => {
1127                Value::Object(Obj::native("deleteTHead", super::super::builtins::dom_table_delete_thead))
1128            }
1129            "deleteTFoot" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "table").unwrap_or(false) => {
1130                Value::Object(Obj::native("deleteTFoot", super::super::builtins::dom_table_delete_tfoot))
1131            }
1132            "deleteCaption" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "table").unwrap_or(false) => {
1133                Value::Object(Obj::native("deleteCaption", super::super::builtins::dom_table_delete_caption))
1134            }
1135            // `input.type` の読み書きが JS プロパティとして未対応だった
1136            // (`getAttribute('type')` 経由の間接アクセスしかできなかった)。
1137            // `fieldset.type`(仕様上、属性値に関わらず常に固定文字列
1138            // `"fieldset"`を返す読み取り専用IDLプロパティ。丸ごと未対応
1139            // だった。汎用の属性直結`type`ゲッターに落ちると空文字列に
1140            // なってしまうため、こちらを先に処理する。2026-07-17 発見・
1141            // 実装)。
1142            "type" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "fieldset").unwrap_or(false) => {
1143                Value::str("fieldset")
1144            }
1145            // `textarea.type`(仕様上、属性値に関わらず常に固定文字列
1146            // `"textarea"`を返す読み取り専用IDLプロパティ。`fieldset.type`
1147            // と同じ理由で丸ごと未対応だった。2026-07-17 発見・実装)。
1148            "type" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "textarea").unwrap_or(false) => {
1149                Value::str("textarea")
1150            }
1151            // `output.type`(仕様上、属性値に関わらず常に固定文字列
1152            // `"output"`を返す読み取り専用IDLプロパティ。`<output>`要素
1153            // 自体が丸ごと未対応だった。`fieldset.type`/`textarea.type`と
1154            // 同じパターン。2026-07-17 発見・実装)。
1155            "type" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "output").unwrap_or(false) => {
1156                Value::str("output")
1157            }
1158            // `output.htmlFor`(`for`属性を空白区切りトークンとして扱う
1159            // `DOMTokenList`。丸ごと未対応だった。`label.htmlFor`は同名だが
1160            // 単一文字列を返す別の意味なので、`relList`/`sandbox`と同じ
1161            // 「読み取り専用トークン配列」簡略実装で区別する。汎用の
1162            // 単一文字列版`htmlFor`アームより先にタグ限定で判定する。
1163            // 2026-07-18 発見・実装)。
1164            "htmlFor" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "output").unwrap_or(false) => {
1165                let v = self.dom.borrow().get_attr(idx, "for").unwrap_or_default();
1166                Value::Object(Obj::array(v.split_whitespace().map(Value::str).collect()))
1167            }
1168            // `select.type`(仕様上、`multiple`属性の有無で`"select-
1169            // multiple"`/`"select-one"`のいずれか固定文字列を返す読み取り
1170            // 専用IDLプロパティ。`fieldset.type`と同じ理由(汎用の属性
1171            // 直結`type`ゲッターに落ちると空文字列になる)で丸ごと未対応
1172            // だった。2026-07-17 発見・実装)。
1173            "type" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "select").unwrap_or(false) => {
1174                if self.dom.borrow().has_attr(idx, "multiple") {
1175                    Value::str("select-multiple")
1176                } else {
1177                    Value::str("select-one")
1178                }
1179            }
1180            // `button.type`(仕様上、`type`属性が無いか不正な値(列挙値
1181            // `"submit"`/`"reset"`/`"button"`以外)の場合は既定値
1182            // `"submit"`にフォールバックする読み取り専用寄りIDLプロパティ
1183            // だが、汎用の属性直結`type`ゲッターに落ちると単に空文字列に
1184            // なってしまい、`if (btn.type === 'submit')`という広く使われる
1185            // 定番チェックが常に偽になる実害のあるバグだった。丸ごと
1186            // 未対応だった。2026-07-17 発見・実装)。
1187            "type" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "button").unwrap_or(false) => {
1188                let v = self.dom.borrow().get_attr(idx, "type").map(|s| s.to_lowercase());
1189                match v.as_deref() {
1190                    Some("reset") => Value::str("reset"),
1191                    Some("button") => Value::str("button"),
1192                    _ => Value::str("submit"),
1193                }
1194            }
1195            // `input.type`(仕様上、`type`属性が無いか既知の列挙値以外
1196            // (不正な値)の場合は既定値`"text"`にフォールバックする
1197            // 読み取り専用寄りIDLプロパティ。`fieldset.type`/`select.type`/
1198            // `button.type`と同じ理由で丸ごと未対応だった。全入力系IDL
1199            // プロパティの中で最も広く参照される`.type`が常に空文字列に
1200            // なっていた実害の大きいバグ。2026-07-17 発見・実装)。
1201            "type" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "input").unwrap_or(false) => {
1202                const KNOWN_TYPES: [&str; 22] = [
1203                    "text", "hidden", "search", "tel", "url", "email", "password", "date", "month",
1204                    "week", "time", "datetime-local", "number", "range", "color", "checkbox", "radio",
1205                    "file", "submit", "image", "reset", "button",
1206                ];
1207                let v = self.dom.borrow().get_attr(idx, "type").map(|s| s.to_lowercase());
1208                match v {
1209                    Some(t) if KNOWN_TYPES.contains(&t.as_str()) => Value::str(t),
1210                    _ => Value::str("text"),
1211                }
1212            }
1213            // `ol.type`(`"1"`/`"a"`/`"A"`/`"i"`/`"I"`の列挙属性。既知値以外・
1214            // 欠落時は仕様上の既定値`"1"`にフォールバックする。`fieldset.
1215            // type`等と同じバグパターンで丸ごと未対応だった。2026-07-17
1216            // 発見・実装)。
1217            "type" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "ol").unwrap_or(false) => {
1218                let v = self.dom.borrow().get_attr(idx, "type");
1219                match v.as_deref() {
1220                    Some("1") => Value::str("1"),
1221                    Some("a") => Value::str("a"),
1222                    Some("A") => Value::str("A"),
1223                    Some("i") => Value::str("i"),
1224                    Some("I") => Value::str("I"),
1225                    _ => Value::str("1"),
1226                }
1227            }
1228            "type" => Value::str(self.dom.borrow().get_attr(idx, "type").unwrap_or_default()),
1229            // `input.checked`(チェックボックス/ラジオボタンの選択状態)が JS プロパティ
1230            // として一切実装されていなかった(`getAttribute('checked')`/`hasAttribute
1231            // ('checked')` 経由の間接アクセスしかできず、`checkbox.checked` や
1232            // `checkbox.checked = true` という最も基本的な書き方が常に `undefined`/
1233            // 黙殺になっていた)。`checked` 属性の有無をそのまま真偽値として公開する。
1234            "checked" => {
1235                let dom = self.dom.borrow();
1236                if let Some(lc) = dom.get_attr(idx, "_live_checked") {
1237                    Value::Bool(lc == "true")
1238                } else {
1239                    Value::Bool(dom.has_attr(idx, "checked"))
1240                }
1241            }
1242            // `input.files`(`<input type="file">`のFileList。丸ごと未対応
1243            // だった。2026-07-17 発見)。この処理系には実際のファイル
1244            // ピッカーUIが無く、JSから見て「ファイルが選択される」経路が
1245            // 一切存在しないため、常に空の配列(空`FileList`相当)を返す
1246            // 誠実な簡略実装(`.readyState`/`.networkState`と同方針)。
1247            "files"
1248                if self.dom.borrow().get_attr(idx, "type").as_deref() == Some("file") =>
1249            {
1250                Value::Object(Obj::array(alloc::vec![]))
1251            }
1252
1253            // `iframe.contentWindow`/`.contentDocument`(丸ごと未対応
1254            // だった。2026-07-17 発見)。この処理系には独立JSレルムを持つ
1255            // iframe(ネストしたブラウジングコンテキスト)が無く常に単一
1256            // フレームのため、`window.top`/`.parent`/`.frames`(2026-07-16
1257            // 追加。同じ「フレームが無いので自分自身を指す」規定を適用
1258            // 済み)と同じ簡略方針で、単一のグローバル`window`/`document`
1259            // をそのまま返す誠実な近似実装。
1260            "contentWindow"
1261                if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "iframe").unwrap_or(false) =>
1262            {
1263                self.global.borrow().vars.get("window").cloned().unwrap_or(Value::Undefined)
1264            }
1265            // `object.contentDocument`(丸ごと未対応だった。`HTMLObject
1266            // Element`は`contentWindow`を持たず`contentDocument`のみが
1267            // 仕様上の対象。2026-07-17 発見)。`iframe`と同じ「フレームが
1268            // 無いので自分自身を指す」簡略方針を適用。
1269            "contentDocument"
1270                if self
1271                    .dom
1272                    .borrow()
1273                    .nodes
1274                    .get(idx)
1275                    .map(|n| n.tag == "iframe" || n.tag == "object")
1276                    .unwrap_or(false) =>
1277            {
1278                self.global.borrow().vars.get("document").cloned().unwrap_or(Value::Undefined)
1279            }
1280            // `object.data`(`href`/`src`/`action`と同じくURL属性を反映する
1281            // IDLプロパティで、生の属性文字列ではなく現在のページURL基準で
1282            // 解決した絶対URLを返す必要がある。テキストノードの
1283            // `CharacterData.data`と同名で衝突するため、`is_text`ガード付き
1284            // の既存`data`アームより先にタグ限定で判定する。丸ごと未対応
1285            // だった。2026-07-17 発見・実装)。
1286            "data" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "object").unwrap_or(false) => {
1287                let raw = self.dom.borrow().get_attr(idx, "data").unwrap_or_default();
1288                Value::str(self.resolve_url_against_page(&raw))
1289            }
1290            // `dialog.open`(`<dialog>` が丸ごと未対応だった。`open` 属性の有無を
1291            // そのまま真偽値として公開する。`checked` と同じパターン)。
1292            "open" => Value::Bool(self.dom.borrow().has_attr(idx, "open")),
1293            // `element.inert`(HTML5 グローバル真偽属性。丸ごと未対応だった。`checked`
1294            // と同じパターン。実際のポインタ操作ブロックは CSS 側の UA 既定
1295            // `pointer-events: none` 配線で実現、詳細は css.rs 参照)。
1296            "inert" => Value::Bool(self.dom.borrow().has_attr(idx, "inert")),
1297            // `dialog.returnValue`(`close(v)` が内部専用属性 `_return_value` に
1298            // 書き込んだ値を読む。未設定なら仕様どおり空文字列)。
1299            "returnValue" => Value::str(
1300                self.dom
1301                    .borrow()
1302                    .get_attr(idx, "_return_value")
1303                    .unwrap_or_default(),
1304            ),
1305            // `input.indeterminate`(checkbox の不確定表示状態)が丸ごと未対応
1306            // だった。HTML 属性としては反映されない JS 専用プロパティ(仕様どおり
1307            // `hasAttribute('indeterminate')` は常に `false` のまま)のため、内部
1308            // 専用の `_indeterminate` 属性キーで状態を持つ(`:indeterminate` CSS
1309            // 疑似クラスもこのキーを直接見る)。
1310            "indeterminate" => Value::Bool(self.dom.borrow().has_attr(idx, "_indeterminate")),
1311            // `input.disabled`/`option.selected`(属性の有無を真偽値として公開する
1312            // ブール型プロパティ)と `input.placeholder`/`input.name`(文字列型
1313            // プロパティ)が、`checked`/`type` と同じ理由で JS プロパティとして
1314            // 丸ごと未実装だった(`button.disabled = true` という定番パターンが
1315            // 常に黙殺されていた)。
1316            // `<fieldset disabled>`配下のコントロールは自身に`disabled`属性が無くても
1317            // 暗黙に無効化される(HTML5仕様。`is_disabled`は祖先の`<fieldset
1318            // disabled>`も遡って見る。詳細はそちらのコメント参照)。以前は要素自身の
1319            // `disabled`属性の有無しか見ておらず、`<fieldset disabled><input></fieldset>`
1320            // の`input.disabled`が常に`false`になっていた。
1321            "disabled" => Value::Bool(self.dom.borrow().is_disabled(idx)),
1322            "selected" => Value::Bool(self.dom.borrow().has_attr(idx, "selected")),
1323            // `option.index`(`select.selectedIndex`と対になる、`<option>`側の
1324            // 0始まり位置を返すIDL属性。丸ごと未対応だった)。`<option>`以外の
1325            // タグでは意味を持たないため対象外(他プロパティと同じ簡略方針)。
1326            "index"
1327                if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "option").unwrap_or(false) =>
1328            {
1329                Value::Number(self.dom.borrow().option_index(idx) as f64)
1330            }
1331            "placeholder" => {
1332                Value::str(self.dom.borrow().get_attr(idx, "placeholder").unwrap_or_default())
1333            }
1334            // `input.pattern`(正規表現バリデーション制約文字列。`placeholder`
1335            // と同じ単純な文字列反映だが丸ごと未対応だった。`checkValidity`/
1336            // `.validity.patternMismatch`側は`pattern_matches`経由で既に
1337            // `pattern`属性を評価済みだったが、JSプロパティとして
1338            // `input.pattern`単体で読み書きする経路が漏れていた。
1339            // 2026-07-17 発見・実装)。
1340            "pattern" => Value::str(self.dom.borrow().get_attr(idx, "pattern").unwrap_or_default()),
1341            "name" => Value::str(self.dom.borrow().get_attr(idx, "name").unwrap_or_default()),
1342            // `input.required`/`input.readOnly`/`input.multiple`(属性直結のブール型
1343            // プロパティ)と `img.src`/`img.alt`/`a.href`(文字列型プロパティ)も、
1344            // `disabled`/`placeholder` と同じ理由で JS プロパティとして丸ごと未実装
1345            // だった。`readOnly` は DOM の慣例どおり属性名 `readonly`(小文字)に対応する。
1346            "required" => Value::Bool(self.dom.borrow().has_attr(idx, "required")),
1347            // `form.noValidate`(`novalidate` 属性の camelCase プロパティ版)が丸ごと
1348            // 未対応だった。`WebEngine::submit_form` は属性の有無を直接見て送信前検証を
1349            // スキップしていたため実挙動には既に配線済みだったが、JS からプログラム的に
1350            // `form.noValidate = true` で検証を無効化する定番パターンだけができなかった。
1351            "noValidate" => Value::Bool(self.dom.borrow().has_attr(idx, "novalidate")),
1352            "readOnly" => Value::Bool(self.dom.borrow().has_attr(idx, "readonly")),
1353            "multiple" => Value::Bool(self.dom.borrow().has_attr(idx, "multiple")),
1354            // `input.capture`(`type="file"`のカメラ/マイク取り込みヒント。
1355            // `"user"`/`"environment"`を指す単純な文字列反映。実際の
1356            // カメラ起動機構は無いため属性値の反映のみ行う簡略実装だが
1357            // 丸ごと未対応だった。2026-07-17 発見・実装)。
1358            "capture" => Value::str(self.dom.borrow().get_attr(idx, "capture").unwrap_or_default()),
1359            // `input.accept`(`type="file"`が許可するファイル種別フィルタ。
1360            // `capture`と同じ単純な文字列反映だが丸ごと未対応だった。
1361            // 2026-07-18 発見・実装)。
1362            "accept" => Value::str(self.dom.borrow().get_attr(idx, "accept").unwrap_or_default()),
1363            // `src`/`href`/`action`(URL属性を反映する IDL プロパティ)は仕様上、生の
1364            // 属性文字列ではなく現在のページURL基準で解決した絶対URLを返す必要がある
1365            // (`getAttribute('href')` は生の文字列のままでよいが、`.href`/`.src`/
1366            // `.action` は常に絶対URL、という区別)。以前はこの解決が一切行われず
1367            // `<a href="/p">.href` が相対文字列 `"/p"` のまま返る静かなバグだった。
1368            "src" => {
1369                let raw = self.dom.borrow().get_attr(idx, "src").unwrap_or_default();
1370                Value::str(self.resolve_url_against_page(&raw))
1371            }
1372            "srcObject" => Value::Null,
1373            "alt" => Value::str(self.dom.borrow().get_attr(idx, "alt").unwrap_or_default()),
1374            "href" => {
1375                let raw = self.dom.borrow().get_attr(idx, "href").unwrap_or_default();
1376                Value::str(self.resolve_url_against_page(&raw))
1377            }
1378            // `a`/`area`要素のURL分解プロパティ(`URLUtils`ミックスイン。
1379            // `.protocol`/`.host`/`.hostname`/`.port`/`.pathname`/`.search`/
1380            // `.hash`/`.origin`/`.username`/`.password`)が丸ごと未対応
1381            // だった(`location`/`URL`は既に`location_components`で対応
1382            // 済みだったが、`<a>`要素側への配線が漏れていた非対称ギャップ。
1383            // 2026-07-17 発見)。`href`と同じ`resolve_url_against_page`で
1384            // 絶対URL化した上で、`location`/`URL`と同じ`location_components`
1385            // パーサを再利用する。
1386            "protocol" | "host" | "hostname" | "port" | "pathname" | "search" | "hash" | "origin"
1387            | "username" | "password"
1388                if self
1389                    .dom
1390                    .borrow()
1391                    .nodes
1392                    .get(idx)
1393                    .map(|n| n.tag == "a" || n.tag == "area")
1394                    .unwrap_or(false) =>
1395            {
1396                let raw = self.dom.borrow().get_attr(idx, "href").unwrap_or_default();
1397                let abs = self.resolve_url_against_page(&raw);
1398                let comps = super::super::builtins::location_components(&abs);
1399                Value::str(comps.iter().find(|(k, _)| *k == key).map(|(_, v)| v.clone()).unwrap_or_default())
1400            }
1401            // `label.htmlFor`/`form.action`/`form.method`/`input.min`/`max`/`step`/
1402            // `maxLength`/`a.target`/`rel`/`download` も同種の未実装だった。
1403            // `htmlFor`/`maxLength` は DOM の慣例どおりそれぞれ属性名 `for`/`maxlength`
1404            // (小文字・別名)に対応する。
1405            "htmlFor" => Value::str(self.dom.borrow().get_attr(idx, "for").unwrap_or_default()),
1406            // `form.acceptCharset`(`accept-charset`属性のcamelCase IDL
1407            // プロパティ版。`htmlFor`と同じ単純な属性名マッピングだが
1408            // 丸ごと未対応だった。2026-07-17 発見・実装)。
1409            "acceptCharset" => Value::str(self.dom.borrow().get_attr(idx, "accept-charset").unwrap_or_default()),
1410            // `meta.httpEquiv`(`http-equiv`属性のcamelCase IDLプロパティ版。
1411            // `htmlFor`/`acceptCharset`と同じ単純な属性名マッピングだが、
1412            // `meta.content`/`.name`/`.charset`ともども丸ごと未対応
1413            // だった。2026-07-17 発見・実装)。
1414            "httpEquiv" => Value::str(self.dom.borrow().get_attr(idx, "http-equiv").unwrap_or_default()),
1415            "content" => {
1416                let tag = self.dom.borrow().nodes.get(idx).map(|n| n.tag.clone()).unwrap_or_default();
1417                if tag == "meta" {
1418                    Value::str(self.dom.borrow().get_attr(idx, "content").unwrap_or_default())
1419                } else if tag == "template" {
1420                    let existing_attr = self.dom.borrow().get_attr(idx, "_template_content_idx");
1421                    let frag_idx = existing_attr.and_then(|s| s.parse::<usize>().ok());
1422                    let frag_idx = match frag_idx {
1423                        Some(fi) => fi,
1424                        None => {
1425                            let fi = self.dom.borrow_mut().create_element("#document-fragment");
1426                            self.dom.borrow_mut().set_attr(idx, "_template_content_idx", &fi.to_string());
1427                            let direct_children = self.dom.borrow().nodes.get(idx).map(|n| n.children.clone()).unwrap_or_default();
1428                            if !direct_children.is_empty() {
1429                                let mut dom = self.dom.borrow_mut();
1430                                if let Some(template_node) = dom.nodes.get_mut(idx) {
1431                                    template_node.children.clear();
1432                                }
1433                                if let Some(frag_node) = dom.nodes.get_mut(fi) {
1434                                    frag_node.children = direct_children.clone();
1435                                }
1436                                for &child_idx in &direct_children {
1437                                    if let Some(child_node) = dom.nodes.get_mut(child_idx) {
1438                                        child_node.parent = Some(fi);
1439                                    }
1440                                }
1441                            }
1442                            fi
1443                        }
1444                    };
1445                    Value::Object(Obj::dom(frag_idx))
1446                } else {
1447                    Value::str(self.dom.borrow().get_attr(idx, "content").unwrap_or_default())
1448                }
1449            }
1450            "charset" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "meta").unwrap_or(false) => {
1451                Value::str(self.dom.borrow().get_attr(idx, "charset").unwrap_or_default())
1452            }
1453            // `script.charset`(廃止予定だが仕様に残る単純な文字列反映。
1454            // 対になる汎用セッターは既に存在したが、ゲッターが`meta`
1455            // タグ限定のままで他タグ(`script`等)では未対応だった。
1456            // 2026-07-18 発見・実装)。
1457            "charset" => Value::str(self.dom.borrow().get_attr(idx, "charset").unwrap_or_default()),
1458            // `blockquote`/`q`/`ins`/`del.cite`(単純な文字列反映)と
1459            // `ins`/`del`/`time.dateTime`(`datetime`属性のcamelCase IDL
1460            // プロパティ版。`htmlFor`と同じマッピングパターン)が丸ごと
1461            // 未対応だった。2026-07-17 発見・実装)。
1462            "cite" => Value::str(self.dom.borrow().get_attr(idx, "cite").unwrap_or_default()),
1463            "dateTime" => Value::str(self.dom.borrow().get_attr(idx, "datetime").unwrap_or_default()),
1464            "action" => {
1465                let raw = self.dom.borrow().get_attr(idx, "action").unwrap_or_default();
1466                Value::str(self.resolve_url_against_page(&raw))
1467            }
1468            // `form.method`(仕様上、`method`属性が無いか既知の列挙値
1469            // (`"get"`/`"post"`/`"dialog"`)以外の場合は既定値`"get"`に
1470            // フォールバックする読み取り専用寄りIDLプロパティだが、単純な
1471            // 属性直結ゲッターに落ちて空文字列になっていた(`fieldset.
1472            // type`等と同じバグパターン。2026-07-17 発見・実装)。
1473            "method" => {
1474                let v = self.dom.borrow().get_attr(idx, "method").map(|s| s.to_lowercase());
1475                match v.as_deref() {
1476                    Some("post") => Value::str("post"),
1477                    Some("dialog") => Value::str("dialog"),
1478                    _ => Value::str("get"),
1479                }
1480            }
1481            // `form.enctype`/`.encoding`(`encoding`は`enctype`のレガシー
1482            // 別名で同じ属性を指す)が丸ごと未対応だった。仕様上、
1483            // `enctype`属性が無いか既知の列挙値3種以外の場合は既定値
1484            // `"application/x-www-form-urlencoded"`にフォールバックする。
1485            // 2026-07-17 発見・実装)。
1486            "enctype" | "encoding" => {
1487                let v = self.dom.borrow().get_attr(idx, "enctype").map(|s| s.to_lowercase());
1488                match v.as_deref() {
1489                    Some("multipart/form-data") => Value::str("multipart/form-data"),
1490                    Some("text/plain") => Value::str("text/plain"),
1491                    _ => Value::str("application/x-www-form-urlencoded"),
1492                }
1493            }
1494            // `button`/`input` の「送信オーバーライド」IDL プロパティ
1495            // (`formAction`/`formEnctype`/`formMethod`/`formNoValidate`/
1496            // `formTarget`)が丸ごと未対応だった。仕様上、対応する`form*`
1497            // 属性が要素自身にあればそれを使い、無ければ所属フォーム
1498            // (`associated_form`)の同名属性にフォールバックする
1499            // (`formNoValidate`のみ自身の属性のみを反映するブール型)。
1500            // 2026-07-17 発見・実装)。
1501            "formAction"
1502                if self
1503                    .dom
1504                    .borrow()
1505                    .nodes
1506                    .get(idx)
1507                    .map(|n| n.tag == "button" || n.tag == "input")
1508                    .unwrap_or(false) =>
1509            {
1510                let dom = self.dom.borrow();
1511                let raw = dom.get_attr(idx, "formaction").or_else(|| {
1512                    dom.associated_form(idx).and_then(|fi| dom.get_attr(fi, "action"))
1513                }).unwrap_or_default();
1514                drop(dom);
1515                Value::str(self.resolve_url_against_page(&raw))
1516            }
1517            "formEnctype"
1518                if self
1519                    .dom
1520                    .borrow()
1521                    .nodes
1522                    .get(idx)
1523                    .map(|n| n.tag == "button" || n.tag == "input")
1524                    .unwrap_or(false) =>
1525            {
1526                let dom = self.dom.borrow();
1527                let raw = dom.get_attr(idx, "formenctype").or_else(|| {
1528                    dom.associated_form(idx).and_then(|fi| dom.get_attr(fi, "enctype"))
1529                }).map(|s| s.to_lowercase());
1530                match raw.as_deref() {
1531                    Some("multipart/form-data") => Value::str("multipart/form-data"),
1532                    Some("text/plain") => Value::str("text/plain"),
1533                    _ => Value::str("application/x-www-form-urlencoded"),
1534                }
1535            }
1536            "formMethod"
1537                if self
1538                    .dom
1539                    .borrow()
1540                    .nodes
1541                    .get(idx)
1542                    .map(|n| n.tag == "button" || n.tag == "input")
1543                    .unwrap_or(false) =>
1544            {
1545                let dom = self.dom.borrow();
1546                let raw = dom.get_attr(idx, "formmethod").or_else(|| {
1547                    dom.associated_form(idx).and_then(|fi| dom.get_attr(fi, "method"))
1548                }).map(|s| s.to_lowercase());
1549                match raw.as_deref() {
1550                    Some("post") => Value::str("post"),
1551                    Some("dialog") => Value::str("dialog"),
1552                    _ => Value::str("get"),
1553                }
1554            }
1555            "formNoValidate"
1556                if self
1557                    .dom
1558                    .borrow()
1559                    .nodes
1560                    .get(idx)
1561                    .map(|n| n.tag == "button" || n.tag == "input")
1562                    .unwrap_or(false) =>
1563            {
1564                Value::Bool(self.dom.borrow().has_attr(idx, "formnovalidate"))
1565            }
1566            "formTarget"
1567                if self
1568                    .dom
1569                    .borrow()
1570                    .nodes
1571                    .get(idx)
1572                    .map(|n| n.tag == "button" || n.tag == "input")
1573                    .unwrap_or(false) =>
1574            {
1575                let dom = self.dom.borrow();
1576                let raw = dom.get_attr(idx, "formtarget").or_else(|| {
1577                    dom.associated_form(idx).and_then(|fi| dom.get_attr(fi, "target"))
1578                }).unwrap_or_default();
1579                Value::str(raw)
1580            }
1581            "min" => Value::str(self.dom.borrow().get_attr(idx, "min").unwrap_or_default()),
1582            "max" => Value::str(self.dom.borrow().get_attr(idx, "max").unwrap_or_default()),
1583            "step" => Value::str(self.dom.borrow().get_attr(idx, "step").unwrap_or_default()),
1584            // `input`/`textarea.maxLength`/`.minLength`はWebIDL上`long`(数値型)の
1585            // IDL属性で、無指定時の既定値は`-1`だが、`maxLength`は文字列型
1586            // (`Value::str`)で属性が無ければ空文字列を返しており、`minLength`は
1587            // ゲッター自体が丸ごと存在しなかった(`min`/`max`/`step`はIDL定義上も
1588            // 本当に`DOMString`型のため文字列のままで正しいが、`maxLength`/
1589            // `minLength`だけは`long`型という違いを見落としていた)。
1590            "maxLength" => Value::Number(
1591                self.dom
1592                    .borrow()
1593                    .get_attr(idx, "maxlength")
1594                    .and_then(|s| s.trim().parse::<i64>().ok())
1595                    .unwrap_or(-1) as f64,
1596            ),
1597            "minLength" => Value::Number(
1598                self.dom
1599                    .borrow()
1600                    .get_attr(idx, "minlength")
1601                    .and_then(|s| s.trim().parse::<i64>().ok())
1602                    .unwrap_or(-1) as f64,
1603            ),
1604            "target" => Value::str(self.dom.borrow().get_attr(idx, "target").unwrap_or_default()),
1605            "rel" => Value::str(self.dom.borrow().get_attr(idx, "rel").unwrap_or_default()),
1606            "download" => Value::str(self.dom.borrow().get_attr(idx, "download").unwrap_or_default()),
1607            // `area.coords`(単純な文字列反映)と`area.shape`(`"rect"`/
1608            // `"circle"`/`"poly"`/`"default"`の列挙属性。既知値以外・欠落時は
1609            // 仕様上の既定値`"rect"`にフォールバックし、レガシー別名
1610            // `"circ"`/`"polygon"`/`"rectangle"`も正規化する)が丸ごと
1611            // 未対応だった。2026-07-17 発見・実装)。
1612            "coords"
1613                if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "area").unwrap_or(false) =>
1614            {
1615                Value::str(self.dom.borrow().get_attr(idx, "coords").unwrap_or_default())
1616            }
1617            "shape"
1618                if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "area").unwrap_or(false) =>
1619            {
1620                let v = self.dom.borrow().get_attr(idx, "shape").map(|s| s.to_lowercase());
1621                match v.as_deref() {
1622                    Some("circle") | Some("circ") => Value::str("circle"),
1623                    Some("poly") | Some("polygon") => Value::str("poly"),
1624                    Some("default") => Value::str("default"),
1625                    _ => Value::str("rect"),
1626                }
1627            }
1628            // `ol.start`/`.reversed`(丸ごと未対応だった。`start`は仕様上
1629            // `long`型IDLで欠落・不正値時の既定値は`1`。`reversed`は単純な
1630            // ブール属性。2026-07-17 発見・実装)。
1631            "start" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "ol").unwrap_or(false) => {
1632                let v = self
1633                    .dom
1634                    .borrow()
1635                    .get_attr(idx, "start")
1636                    .and_then(|s| s.trim().parse::<i64>().ok());
1637                Value::Number(v.unwrap_or(1) as f64)
1638            }
1639            "reversed" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "ol").unwrap_or(false) => {
1640                Value::Bool(self.dom.borrow().has_attr(idx, "reversed"))
1641            }
1642            // `element.lang`/`.dir`(グローバル属性の JS プロパティ版。`:lang()`/`:dir()`
1643            // CSS 疑似クラス自体は既に対応済みだったが、こちらの属性直結プロパティは
1644            // 他の同種プロパティ群の点検から漏れていた)。
1645            "lang" => Value::str(self.dom.borrow().get_attr(idx, "lang").unwrap_or_default()),
1646            "dir" => Value::str(self.dom.borrow().get_attr(idx, "dir").unwrap_or_default()),
1647            // `element.title`(要素のツールチップ文字列。グローバル属性の JS
1648            // プロパティ版)が丸ごと未対応だった(未マッチキーはこの巨大
1649            // match の末尾 `_ => Value::Undefined` に落ちるだけで、`title`
1650            // 属性を汎用的に反映するフォールバックは存在しないため常に
1651            // `undefined` になっていた。2026-07-16 発見)。`lang`/`dir` と
1652            // 同型の属性直結プロパティとして追加。
1653            "title" => Value::str(self.dom.borrow().get_attr(idx, "title").unwrap_or_default()),
1654            // `option`/`optgroup.label`(丸ごと未対応だった。2026-07-17
1655            // 発見)。`option.label`は仕様上`label`属性が無ければ要素の
1656            // テキスト内容(トリム済み)にフォールバックするが、
1657            // `optgroup.label`は同名でも既定`""`の単純な属性反映のため
1658            // 別のmatch armとして分ける。
1659            "label" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "option").unwrap_or(false) => {
1660                match self.dom.borrow().get_attr(idx, "label") {
1661                    Some(v) => Value::str(v),
1662                    None => Value::str(self.dom.borrow().get_text_content(idx).trim().to_string()),
1663                }
1664            }
1665            "label" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "optgroup").unwrap_or(false) => {
1666                Value::str(self.dom.borrow().get_attr(idx, "label").unwrap_or_default())
1667            }
1668            // `track.label`/`.srclang`(単純な文字列反映)と`track.default`
1669            // (`default`属性の有無を反映するブール型)が丸ごと未対応
1670            // だった。`track.kind`修正時の点検漏れ。2026-07-18 発見・
1671            // 実装)。
1672            "label" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "track").unwrap_or(false) => {
1673                Value::str(self.dom.borrow().get_attr(idx, "label").unwrap_or_default())
1674            }
1675            "srclang" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "track").unwrap_or(false) => {
1676                Value::str(self.dom.borrow().get_attr(idx, "srclang").unwrap_or_default())
1677            }
1678            "default" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "track").unwrap_or(false) => {
1679                Value::Bool(self.dom.borrow().has_attr(idx, "default"))
1680            }
1681            // `element.accessKey`(同じ理由で丸ごと未対応だった)。この
1682            // 処理系にキーボードショートカット実装は無いため実際の挙動には
1683            // 影響しないが、値の読み書き自体は仕様どおり動作させる。
1684            "accessKey" => Value::str(self.dom.borrow().get_attr(idx, "accesskey").unwrap_or_default()),
1685            // `element.translate`(`translate="no"` のみ `false`、それ以外は
1686            // 仕様どおり既定 `true`)。
1687            "translate" => Value::Bool(
1688                self.dom.borrow().get_attr(idx, "translate").as_deref() != Some("no"),
1689            ),
1690            // `element.draggable`(丸ごと未対応だった。2026-07-16 発見)。
1691            // 仕様上は `img`/`a[href]` のみ既定 `true` 他は既定 `false`
1692            // という要素種別依存の複雑な既定値があるが、この処理系に
1693            // ドラッグ&ドロップの実装は無く実際の挙動には影響しないため、
1694            // `translate` と同型の単純な `"true"`/`"false"` 属性直結
1695            // (明示指定が無ければ既定 `false`)とする簡略実装。
1696            "draggable" => Value::Bool(
1697                self.dom.borrow().get_attr(idx, "draggable").as_deref() == Some("true"),
1698            ),
1699            // `element.slot`(named slot 割り当て名。丸ごと未対応だった。
1700            // 2026-07-16 発見)。この処理系は Shadow DOM 自体を未実装だが、
1701            // `slot` content 属性自体は Shadow DOM の有無に関わらず単純な
1702            // 文字列属性として存在するため、`title`/`lang` と同型の
1703            // 属性直結プロパティとして追加(実際のスロット割り当て解決は
1704            // 対象外)。
1705            "slot" => Value::str(self.dom.borrow().get_attr(idx, "slot").unwrap_or_default()),
1706            // `element.tabIndex`(`tabindex` 属性の数値プロパティ版)が同じ理由で
1707            // 丸ごと未対応だった。この処理系にキーボード Tab 順序の実装は無いため
1708            // 実際のフォーカス移動には影響しないが、値の読み書き自体は仕様どおり動作
1709            // させる(`tabindex` 未指定時の既定値は `-1` という簡略化)。
1710            "tabIndex" => Value::Number(
1711                self.dom
1712                    .borrow()
1713                    .get_attr(idx, "tabindex")
1714                    .and_then(|v| v.trim().parse::<f64>().ok())
1715                    .unwrap_or(-1.0),
1716            ),
1717            // `element.contentEditable`/`.isContentEditable`(`contenteditable`
1718            // グローバル属性の JS プロパティ版。丸ごと未対応だった)。この処理系には
1719            // キャレット/テキスト編集機構自体が無い(`::selection` と同じ既知の
1720            // 制約)ため、実際の編集可能化は行わず属性値の反映のみを行う簡略実装。
1721            // 祖先からの継承(属性値 `"inherit"`/未指定時の既定)は非対応。
1722            "contentEditable" => Value::str(
1723                match self.dom.borrow().get_attr(idx, "contenteditable").as_deref() {
1724                    Some("true") | Some("") => "true",
1725                    Some("false") => "false",
1726                    Some("plaintext-only") => "plaintext-only",
1727                    _ => "inherit",
1728                },
1729            ),
1730            "isContentEditable" => Value::Bool(matches!(
1731                self.dom.borrow().get_attr(idx, "contenteditable").as_deref(),
1732                Some("true") | Some("") | Some("plaintext-only")
1733            )),
1734            // `element.spellcheck`(`spellcheck` グローバル属性の JS プロパティ版。
1735            // 丸ごと未対応だった)。この処理系にはスペルチェック機構自体が無いため
1736            // 属性値の反映のみ行う簡略実装。仕様上の既定値は `true`(未指定/`"true"`
1737            // なら有効、`"false"` のみ無効)。
1738            "spellcheck" => Value::Bool(
1739                self.dom.borrow().get_attr(idx, "spellcheck").as_deref() != Some("false"),
1740            ),
1741            // `element.inputMode`/`.enterKeyHint`(`inputmode`/`enterkeyhint`
1742            // グローバル属性の JS プロパティ版。丸ごと未対応だった)。この処理系には
1743            // 仮想キーボード自体が無いため属性値の反映のみ行う簡略実装。
1744            "inputMode" => Value::str(self.dom.borrow().get_attr(idx, "inputmode").unwrap_or_default()),
1745            // `select.size`(欠落・不正値時の既定値`0`。表示行数を指定する
1746            // `unsigned long`型IDLプロパティ)と`input.size`(欠落・不正値
1747            // 時の既定値`20`。テキストフィールドの表示幅を文字数で指定する
1748            // `unsigned long`型IDLプロパティ)が丸ごと未対応だった。
1749            // `fieldset.type`等と同じバグパターン。2026-07-18 発見・実装)。
1750            "size" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "select").unwrap_or(false) => {
1751                let v = self
1752                    .dom
1753                    .borrow()
1754                    .get_attr(idx, "size")
1755                    .and_then(|s| s.trim().parse::<i64>().ok())
1756                    .filter(|n| *n >= 0)
1757                    .unwrap_or(0);
1758                Value::Number(v as f64)
1759            }
1760            "size" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "input").unwrap_or(false) => {
1761                let v = self
1762                    .dom
1763                    .borrow()
1764                    .get_attr(idx, "size")
1765                    .and_then(|s| s.trim().parse::<i64>().ok())
1766                    .filter(|n| *n >= 1)
1767                    .unwrap_or(20);
1768                Value::Number(v as f64)
1769            }
1770            // `textarea.rows`/`.cols`(欠落・不正値時の既定値がそれぞれ`2`/
1771            // `20`の`unsigned long`型IDLプロパティ。`select.size`等と同じ
1772            // バグパターンで丸ごと未対応だった。2026-07-18 発見・実装)。
1773            "rows" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "textarea").unwrap_or(false) => {
1774                let v = self
1775                    .dom
1776                    .borrow()
1777                    .get_attr(idx, "rows")
1778                    .and_then(|s| s.trim().parse::<i64>().ok())
1779                    .filter(|n| *n >= 1)
1780                    .unwrap_or(2);
1781                Value::Number(v as f64)
1782            }
1783            "cols" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "textarea").unwrap_or(false) => {
1784                let v = self
1785                    .dom
1786                    .borrow()
1787                    .get_attr(idx, "cols")
1788                    .and_then(|s| s.trim().parse::<i64>().ok())
1789                    .filter(|n| *n >= 1)
1790                    .unwrap_or(20);
1791                Value::Number(v as f64)
1792            }
1793            // `input`/`select`/`textarea`/`form.autocomplete`(単純な文字列
1794            // 反映)と`element.autofocus`(ブール型。フォーカス自動移動の
1795            // 実機構は無いため属性値の反映のみ行う簡略実装)が丸ごと
1796            // 未対応だった。2026-07-18 発見・実装)。
1797            "autocomplete" => Value::str(self.dom.borrow().get_attr(idx, "autocomplete").unwrap_or_default()),
1798            "autofocus" => Value::Bool(self.dom.borrow().has_attr(idx, "autofocus")),
1799            "enterKeyHint" => {
1800                Value::str(self.dom.borrow().get_attr(idx, "enterkeyhint").unwrap_or_default())
1801            }
1802            // `element.autocapitalize`(`autocapitalize` グローバル属性の JS
1803            // プロパティ版。丸ごと未対応だった)。仮想キーボード自体が無いため
1804            // `inputMode`/`enterKeyHint` と同じ単純な文字列反映の簡略実装。
1805            "autocapitalize" => {
1806                Value::str(self.dom.borrow().get_attr(idx, "autocapitalize").unwrap_or_default())
1807            }
1808            // `audio`/`video.autoplay`/`.controls`/`.loop`/`.muted`/
1809            // `.playsInline`(HTMLMediaElement の真偽属性群。丸ごと未対応
1810            // だった)。`.play()`等と同じく実際の音声/映像出力機構は無いため
1811            // 属性値の反映のみ行う簡略実装(`disabled`等と同型パターン)。
1812            "autoplay" => Value::Bool(self.dom.borrow().has_attr(idx, "autoplay")),
1813            "controls" => Value::Bool(self.dom.borrow().has_attr(idx, "controls")),
1814            "loop" => Value::Bool(self.dom.borrow().has_attr(idx, "loop")),
1815            "muted" => Value::Bool(self.dom.borrow().has_attr(idx, "muted")),
1816            "playsInline" => Value::Bool(self.dom.borrow().has_attr(idx, "playsinline")),
1817            // `video.disablePictureInPicture`(`playsInline`と同じ単純な
1818            // ブール属性反映だが丸ごと未対応だった)と`iframe.allowFullscreen`
1819            // (`allowfullscreen`属性のcamelCase IDLプロパティ版)。
1820            // 2026-07-17 発見・実装)。
1821            "disablePictureInPicture" => {
1822                Value::Bool(self.dom.borrow().has_attr(idx, "disablepictureinpicture"))
1823            }
1824            // `video.disableRemotePlayback`(`disablePictureInPicture`と同じ
1825            // 単純なブール属性反映だが丸ごと未対応だった。2026-07-18
1826            // 発見・実装)。
1827            "disableRemotePlayback" => {
1828                Value::Bool(self.dom.borrow().has_attr(idx, "disableremoteplayback"))
1829            }
1830            "allowFullscreen" => Value::Bool(self.dom.borrow().has_attr(idx, "allowfullscreen")),
1831            // `iframe.credentialless`(COEP credentialless。`allowFullscreen`と
1832            // 同じ単純なブール属性反映だが丸ごと未対応だった。2026-07-18
1833            // 発見・実装)。
1834            "credentialless" => Value::Bool(self.dom.borrow().has_attr(idx, "credentialless")),
1835            // `canvas.width`/`.height`(`unsigned long`型IDL属性。欠落・
1836            // 不正値時の既定値は仕様上それぞれ`300`/`150`)と
1837            // `img`/`video.width`/`.height`(同じく`unsigned long`型だが
1838            // 既定値は`0`)が丸ごと未対応だった。`embed`/`object`/`iframe`
1839            // の`width`/`height`は仕様上`DOMString`型で意味が異なるため
1840            // 対象外(既知ギャップとしてTODO.mdに記載)。2026-07-17
1841            // 発見・実装)。
1842            "width" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "canvas").unwrap_or(false) => {
1843                let v = self
1844                    .dom
1845                    .borrow()
1846                    .get_attr(idx, "width")
1847                    .and_then(|s| s.trim().parse::<i64>().ok())
1848                    .filter(|n| *n >= 0)
1849                    .unwrap_or(300);
1850                Value::Number(v as f64)
1851            }
1852            "height" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "canvas").unwrap_or(false) => {
1853                let v = self
1854                    .dom
1855                    .borrow()
1856                    .get_attr(idx, "height")
1857                    .and_then(|s| s.trim().parse::<i64>().ok())
1858                    .filter(|n| *n >= 0)
1859                    .unwrap_or(150);
1860                Value::Number(v as f64)
1861            }
1862            "getContext" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "canvas").unwrap_or(false) => {
1863                Value::Object(Obj::native("getContext", super::super::builtins::dom_canvas_get_context))
1864            }
1865            "toDataURL" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "canvas").unwrap_or(false) => {
1866                Value::Object(Obj::native("toDataURL", super::super::builtins::dom_canvas_to_data_url))
1867            }
1868            "toBlob" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "canvas").unwrap_or(false) => {
1869                Value::Object(Obj::native("toBlob", super::super::builtins::dom_canvas_to_blob))
1870            }
1871            "width" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "img" || n.tag == "video").unwrap_or(false) => {
1872                let v = self
1873                    .dom
1874                    .borrow()
1875                    .get_attr(idx, "width")
1876                    .and_then(|s| s.trim().parse::<i64>().ok())
1877                    .filter(|n| *n >= 0)
1878                    .unwrap_or(0);
1879                Value::Number(v as f64)
1880            }
1881            "height" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "img" || n.tag == "video").unwrap_or(false) => {
1882                let v = self
1883                    .dom
1884                    .borrow()
1885                    .get_attr(idx, "height")
1886                    .and_then(|s| s.trim().parse::<i64>().ok())
1887                    .filter(|n| *n >= 0)
1888                    .unwrap_or(0);
1889                Value::Number(v as f64)
1890            }
1891            // `embed`/`object`/`iframe.width`/`.height`(`canvas`/`img`/
1892            // `video.width`/`.height`とは異なり、仕様上`DOMString`型の
1893            // レガシー予約済み属性で単純な文字列反映(数値クランプ・既定値
1894            // フォールバック無し)。`spec/TODO.md`に記載していた既知
1895            // ギャップを解消。2026-07-17 発見・実装)。
1896            "width"
1897                if self
1898                    .dom
1899                    .borrow()
1900                    .nodes
1901                    .get(idx)
1902                    .map(|n| n.tag == "embed" || n.tag == "object" || n.tag == "iframe")
1903                    .unwrap_or(false) =>
1904            {
1905                Value::str(self.dom.borrow().get_attr(idx, "width").unwrap_or_default())
1906            }
1907            "height"
1908                if self
1909                    .dom
1910                    .borrow()
1911                    .nodes
1912                    .get(idx)
1913                    .map(|n| n.tag == "embed" || n.tag == "object" || n.tag == "iframe")
1914                    .unwrap_or(false) =>
1915            {
1916                Value::str(self.dom.borrow().get_attr(idx, "height").unwrap_or_default())
1917            }
1918            // `audio`/`video.paused`/`.currentTime`/`.duration`/`.volume`/
1919            // `.playbackRate`/`.ended`(HTMLMediaElement の再生状態群。丸ごと
1920            // 未対応だった)。この処理系には実際のデコード/再生パイプラインが
1921            // 無いため、`.play()`/`.pause()`(内部専用属性 `_paused` を切替)と
1922            // 同じ「状態だけを追跡する」簡略実装とする。`duration` は実際の
1923            // メディア長を計測できないため仕様どおり `NaN`(未ロード時の既定
1924            // 値)を返す誠実な簡略実装。
1925            // 【2026-10-17】再生中(または再生した)要素は、再生スレッドの実際の状態
1926            // (`mse::PLAYBACK`)を返す。それ以外は従来どおり内部属性。
1927            "paused" => match self.media_status(idx) {
1928                Some(st) if st.playing => Value::Bool(false),
1929                Some(st) if st.ended => Value::Bool(true),
1930                _ => Value::Bool(self.dom.borrow().get_attr(idx, "_paused").as_deref() != Some("false")),
1931            },
1932            "currentTime" => match self.media_status(idx) {
1933                Some(st) => Value::Number(st.current_time),
1934                None => Value::Number(
1935                    self.dom
1936                        .borrow()
1937                        .get_attr(idx, "_current_time")
1938                        .and_then(|v| v.parse::<f64>().ok())
1939                        .unwrap_or(0.0),
1940                ),
1941            },
1942            "duration" => {
1943                let src = self.dom.borrow().get_attr(idx, "src").unwrap_or_default();
1944                Value::Number(self.media_source_duration(&src).unwrap_or(f64::NAN))
1945            }
1946            "volume" => Value::Number(
1947                self.dom
1948                    .borrow()
1949                    .get_attr(idx, "_volume")
1950                    .and_then(|v| v.parse::<f64>().ok())
1951                    .unwrap_or(1.0),
1952            ),
1953            "playbackRate" => Value::Number(
1954                self.dom
1955                    .borrow()
1956                    .get_attr(idx, "_playback_rate")
1957                    .and_then(|v| v.parse::<f64>().ok())
1958                    .unwrap_or(1.0),
1959            ),
1960            "ended" => match self.media_status(idx) {
1961                Some(st) => Value::Bool(st.ended),
1962                None => Value::Bool(self.dom.borrow().has_attr(idx, "_ended")),
1963            },
1964            // `audio`/`video.readyState`/`.networkState`/`.error`(丸ごと
1965            // 未対応だった)。この処理系には実際のネットワーク層/デコーダが無く
1966            // メディアデータを一切ロードしないため、`src`/`srcset` 未指定なら
1967            // `NETWORK_EMPTY`(0)、指定済みでも実際には取得しないため
1968            // `NETWORK_NO_SOURCE`(3)を返す誠実な簡略実装(`navigator.
1969            // geolocation` と同じ「できないことを楽観視しない」方針)。
1970            // `readyState` は常に `HAVE_NOTHING`(0)、`error` は常に `null`。
1971            // MSE の要素は SourceBuffer の中身から(初期化セグメントだけ = HAVE_METADATA 1、
1972            // メディアデータもある = HAVE_ENOUGH_DATA 4)。
1973            "readyState" => {
1974                let src = self.dom.borrow().get_attr(idx, "src").unwrap_or_default();
1975                Value::Number(super::super::builtins::media_source_ready_state(&src).unwrap_or(0) as f64)
1976            }
1977            "networkState" => {
1978                let src = self.dom.borrow().get_attr(idx, "src").unwrap_or_default();
1979                if src.starts_with("blob:mediasource-") {
1980                    let ended = self.media_status(idx).is_some_and(|st| st.ended);
1981                    Value::Number(if ended { 1.0 } else { 2.0 })
1982                } else {
1983                    let has_src = !src.is_empty() || self.dom.borrow().has_attr(idx, "srcset");
1984                    Value::Number(if has_src { 3.0 } else { 0.0 })
1985                }
1986            }
1987            "error" => Value::Null,
1988            // `script`/`style.nonce`(CSP nonce。丸ごと未対応だった)。この
1989            // 処理系には実際の CSP 検証機構が無いため、単純な文字列反映の
1990            // 簡略実装とする(仕様上は `nonce` content 属性を読めなくする
1991            // セキュリティ対策があるが、IDL プロパティ経由の読み書きは通す)。
1992            "nonce" => Value::str(self.dom.borrow().get_attr(idx, "nonce").unwrap_or_default()),
1993            // `element.role`/`.ariaLabel`/`.ariaHidden`等(`ARIAMixin`。
1994            // `role`/`aria-*`属性のIDLプロパティ版)が丸ごと未対応
1995            // だった。`aria-*`側の命名規則は単純で、`aria`接頭辞の後の
1996            // PascalCase部分を全て小文字化するだけで対応する属性名になる
1997            // (例: `ariaValueNow`→`aria-valuenow`、`ariaLabelledBy`→
1998            // `aria-labelledby`。内部のダッシュは無い)。個別に50近く
1999            // ある属性を1つずつ列挙する代わりに、この規則をそのまま
2000            // コードにした汎用アームで一括対応する。2026-07-18
2001            // 発見・実装)。
2002            "role" => match self.dom.borrow().get_attr(idx, "role") {
2003                Some(v) => Value::str(v),
2004                None => Value::Null,
2005            },
2006            // `ariaActiveDescendantElement`(ARIA Element Reflection。
2007            // `aria-activedescendant`のidref文字列ではなく実際のElement参照を
2008            // 返す/設定する。`popoverTargetElement`と全く同じidref解決
2009            // パターン。汎用の`aria-*`文字列反映アームより前に置く必要がある
2010            // (`starts_with("aria")`の汎用ガードに先に吸われてしまうため)。
2011            // 丸ごと未対応だった。2026-07-18 発見・実装)。
2012            "ariaActiveDescendantElement" => {
2013                let dom = self.dom.borrow();
2014                match dom.get_attr(idx, "aria-activedescendant").filter(|f| !f.is_empty()) {
2015                    Some(target_id) => match dom.get_element_by_id(&target_id) {
2016                        Some(ti) => Value::Object(Obj::dom(ti)),
2017                        None => Value::Null,
2018                    },
2019                    None => Value::Null,
2020                }
2021            }
2022            // `ariaLabelledByElements`/`ariaDescribedByElements`/
2023            // `ariaControlsElements`/`ariaOwnsElements`/`ariaFlowToElements`/
2024            // `ariaDetailsElements`/`ariaErrorMessageElements`(ARIA Element
2025            // Reflection。空白区切りidref属性を、文字列ではなく実際の
2026            // Element参照の配列として返す。単数の`ariaActiveDescendantElement`
2027            // と同じ理由で汎用の`aria-*`文字列反映アームより前に置く必要が
2028            // ある。`get_element_by_id`で解決できないid(未接続/存在しない)
2029            // は配列から除外する(仕様は`null`要素を含むが、この処理系の
2030            // 簡略配列表現では穴を開けられないため)。丸ごと未対応だった。
2031            // 2026-07-18 発見・実装)。
2032            _ if key.starts_with("aria") && key.ends_with("Elements") && key.len() > 12 => {
2033                let suffix: String =
2034                    key.chars().skip(4).take(key.len() - 4 - 8).collect::<String>().to_lowercase();
2035                let attr = alloc::format!("aria-{}", suffix);
2036                let dom = self.dom.borrow();
2037                let ids = dom.get_attr(idx, &attr).unwrap_or_default();
2038                let elems: Vec<Value> = ids
2039                    .split_whitespace()
2040                    .filter_map(|id| dom.get_element_by_id(id))
2041                    .map(|ti| Value::Object(Obj::dom(ti)))
2042                    .collect();
2043                Value::Object(Obj::array(elems))
2044            }
2045            _ if key.starts_with("aria") && key.as_bytes().get(4).is_some_and(u8::is_ascii_uppercase) => {
2046                let suffix: String = key.chars().skip(4).collect::<String>().to_lowercase();
2047                let attr = alloc::format!("aria-{}", suffix);
2048                match self.dom.borrow().get_attr(idx, &attr) {
2049                    Some(v) => Value::str(v),
2050                    None => Value::Null,
2051                }
2052            }
2053            // `audio`/`video.buffered`/`.seekable`(`TimeRanges`。丸ごと未対応
2054            // だった)。実際にはメディアデータを一切ロード/シークしないため、
2055            // 常に空の `TimeRanges`(`length:0`、`start`/`end` は範囲外
2056            // アクセスとして例外を投げる)を返す誠実な簡略実装。
2057            // 【2026-10-18】MSE の要素は SourceBuffer の実際の範囲を返す(空だと YouTube の
2058            // player は「読み込み中」のままスピナーを出し続ける)。
2059            "buffered" if self.dom.borrow().get_attr(idx, "src").is_some_and(|s| s.starts_with("blob:mediasource-")) => {
2060                let src = self.dom.borrow().get_attr(idx, "src").unwrap_or_default();
2061                super::super::builtins::media_element_buffered(&src)
2062                    .unwrap_or_else(|| Value::Object(super::super::builtins::make_empty_time_ranges()))
2063            }
2064            "seekable" if self.dom.borrow().get_attr(idx, "src").is_some_and(|s| s.starts_with("blob:mediasource-")) => {
2065                let src = self.dom.borrow().get_attr(idx, "src").unwrap_or_default();
2066                super::super::builtins::media_element_seekable(self.media_source_duration(&src))
2067            }
2068            "played" => match self.media_status(idx) {
2069                Some(st) if st.current_time > 0.0 => {
2070                    super::super::builtins::media_element_seekable(Some(st.current_time))
2071                }
2072                _ => Value::Object(super::super::builtins::make_empty_time_ranges()),
2073            },
2074            "buffered" | "seekable" => {
2075                Value::Object(super::super::builtins::make_empty_time_ranges())
2076            }
2077            // `video.videoWidth`/`.videoHeight`: MSE の場合は SourceBuffer の初期化セグメント
2078            // から得られた映像寸法を返し、それ以外は `0` を返す。
2079            "videoWidth" | "videoHeight"
2080                if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "video").unwrap_or(false) =>
2081            {
2082                let src = self.dom.borrow().get_attr(idx, "src").unwrap_or_default();
2083                if src.starts_with("blob:mediasource-") {
2084                    if let Some((w, h)) = super::super::builtins::media_source_video_dimensions(&src) {
2085                        return Value::Number(if key == "videoWidth" { w as f64 } else { h as f64 });
2086                    }
2087                }
2088                Value::Number(0.0)
2089            }
2090            "seeking" => Value::Bool(false),
2091            // `video.getVideoPlaybackQuality()`: 再生状態(`mse::PLAYBACK`)の累計フレーム数・
2092            // ドロップフレーム数を返す。
2093            "getVideoPlaybackQuality"
2094                if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "video").unwrap_or(false) =>
2095            {
2096                Value::Object(Obj::native("getVideoPlaybackQuality", |_, _, _| {
2097                    let (total, dropped) = {
2098                        let pb = crate::os_lib::mse::PLAYBACK.lock();
2099                        (pb.total_video_frames as f64, pb.dropped_video_frames as f64)
2100                    };
2101                    let q = Obj::plain();
2102                    {
2103                        let mut b = q.borrow_mut();
2104                        b.props.insert("creationTime".into(), Value::Number(0.0));
2105                        b.props.insert("totalVideoFrames".into(), Value::Number(total));
2106                        b.props.insert("droppedVideoFrames".into(), Value::Number(dropped));
2107                        b.props.insert("corruptedVideoFrames".into(), Value::Number(0.0));
2108                    }
2109                    Ok(Value::Object(q))
2110                }))
2111            }
2112            // `audio`/`video.defaultPlaybackRate`(丸ごと未対応だった)。仕様上
2113            // `playbackRate`とは独立した既定値の保持先だが、この処理系には
2114            // 「再生開始時に既定値へリセットする」処理自体が無いため、単純に
2115            // 別の内部専用属性へ保存するだけの簡略実装。
2116            "defaultPlaybackRate" => Value::Number(
2117                self.dom
2118                    .borrow()
2119                    .get_attr(idx, "_default_playback_rate")
2120                    .and_then(|v| v.parse::<f64>().ok())
2121                    .unwrap_or(1.0),
2122            ),
2123            // `audio`/`video.preload`(既知の列挙値`"none"`/`"metadata"`/
2124            // `"auto"`のうち、属性が無ければ仕様上の欠落時既定値`"metadata"`、
2125            // 既知値以外なら不正値既定値`"auto"`にフォールバックする必要が
2126            // あるが、単純な属性直結ゲッターに落ちて空文字列になっていた
2127            // (`fieldset.type`等と同じバグパターン。2026-07-17 発見・実装)。
2128            "preload" => {
2129                let v = self.dom.borrow().get_attr(idx, "preload").map(|s| s.to_lowercase());
2130                match v.as_deref() {
2131                    None => Value::str("metadata"),
2132                    Some("none") => Value::str("none"),
2133                    Some("metadata") => Value::str("metadata"),
2134                    Some(_) => Value::str("auto"),
2135                }
2136            }
2137            // `video.poster`(丸ごと未対応だった。単純な文字列反映)。
2138            "poster" => Value::str(self.dom.borrow().get_attr(idx, "poster").unwrap_or_default()),
2139            // `track.kind`(`"subtitles"`/`"captions"`/`"descriptions"`/
2140            // `"chapters"`/`"metadata"`の列挙属性。`preload`と同じく
2141            // 「欠落時既定値」と「不正値既定値」が異なる特殊なケース
2142            // (欠落時は`"subtitles"`、既知値以外の不正値は`"metadata"`)。
2143            // プロパティ自体が丸ごと未対応だった。2026-07-17 発見・実装)。
2144            "kind" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "track").unwrap_or(false) => {
2145                let v = self.dom.borrow().get_attr(idx, "kind").map(|s| s.to_lowercase());
2146                match v.as_deref() {
2147                    None => Value::str("subtitles"),
2148                    Some("subtitles") => Value::str("subtitles"),
2149                    Some("captions") => Value::str("captions"),
2150                    Some("descriptions") => Value::str("descriptions"),
2151                    Some("chapters") => Value::str("chapters"),
2152                    Some(_) => Value::str("metadata"),
2153                }
2154            }
2155            // `a.ping`/`.hreflang`(丸ごと未対応だった。単純な文字列反映。
2156            // 実際の ping 送信/言語ヒント処理は対象外)。
2157            "ping" => Value::str(self.dom.borrow().get_attr(idx, "ping").unwrap_or_default()),
2158            "hreflang" => Value::str(self.dom.borrow().get_attr(idx, "hreflang").unwrap_or_default()),
2159            // `img`/`source`/`link.srcset`/`.sizes`(レスポンシブ画像の候補
2160            // リスト/表示サイズヒント。丸ごと未対応だった)。この処理系には実際の
2161            // 候補選択アルゴリズムが無いため属性値の反映のみ行う簡略実装。
2162            "srcset" => Value::str(self.dom.borrow().get_attr(idx, "srcset").unwrap_or_default()),
2163            // `link.sizes`は`img`/`source.sizes`(レスポンシブ画像の表示
2164            // サイズヒント、単純なDOMString)とは異なり、仕様上`DOMTokenList`
2165            // (アイコンサイズ`"16x16 32x32"`等の空白区切りトークン集合)。
2166            // 同名の属性を共有タグ間で単純な文字列反映アームに一括で
2167            // 落としていたため、`link`側だけ型が誤っていた(`part`/
2168            // `relList`と同じ「読み取り専用トークン配列」簡略実装で
2169            // 区別する)。2026-07-18 発見・実装)。
2170            "sizes" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "link").unwrap_or(false) => {
2171                let v = self.dom.borrow().get_attr(idx, "sizes").unwrap_or_default();
2172                Value::Object(Obj::array(v.split_whitespace().map(Value::str).collect()))
2173            }
2174            "sizes" => Value::str(self.dom.borrow().get_attr(idx, "sizes").unwrap_or_default()),
2175            // `link`/`style`/`source.media`(メディアクエリ文字列。丸ごと未対応
2176            // だった)。この処理系はこの属性に基づく実際の適用可否判定を行わない
2177            // ため属性値の反映のみ行う。
2178            "media" => Value::str(self.dom.borrow().get_attr(idx, "media").unwrap_or_default()),
2179            // `img.complete`/`.naturalWidth`/`.naturalHeight`(丸ごと未対応
2180            // だった。2026-07-17 発見)。実際の画像デコード(`web_engine/
2181            // image.rs`の`decode_image`)はレンダラ側で完結しており、
2182            // デコード後の実ピクセル寸法がDOMブリッジへ書き戻される経路が
2183            // 無いため、`naturalWidth`/`naturalHeight`は`buffered`/
2184            // `TimeRanges`と同方針の「状態追跡なし」の誠実な簡略実装として
2185            // 常に`0`を返す。`complete`はこの処理系がページ全体を同期的に
2186            // パースしてから`<script>`を実行する(`readyState`と同じ前提)
2187            // ため、`src`属性が設定されていれば「ロード試行済み」として
2188            // `true`を返す簡略実装(実デコード成否は反映しない)。
2189            "complete" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "img").unwrap_or(false) => {
2190                Value::Bool(self.dom.borrow().get_attr(idx, "src").map(|s| !s.is_empty()).unwrap_or(false))
2191            }
2192            "naturalWidth" | "naturalHeight"
2193                if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "img").unwrap_or(false) =>
2194            {
2195                Value::Number(0.0)
2196            }
2197            // `img.useMap`/`.isMap`(イメージマップ関連。丸ごと未対応だった)。
2198            "useMap" => Value::str(self.dom.borrow().get_attr(idx, "usemap").unwrap_or_default()),
2199            // `img.decode()`(丸ごと未対応だった。`complete`と同じ前提で、
2200            // `src`があれば解決、無ければ拒否するPromiseを返す簡略実装。
2201            // 2026-07-17 発見・実装)。
2202            "decode" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "img").unwrap_or(false) => {
2203                Value::Object(Obj::native("decode", super::super::builtins::dom_img_decode))
2204            }
2205            "isMap" => Value::Bool(self.dom.borrow().has_attr(idx, "ismap")),
2206            // `img.currentSrc`(読み取り専用。レスポンシブ画像の候補選択
2207            // アルゴリズムの結果を表すが、この処理系にはその機構が無いため
2208            // `src` 属性をそのまま返す簡略実装。`srcset` のみ指定され `src` が
2209            // 無い場合は空文字列のまま=仕様上のフォールバックは対象外)。
2210            "currentSrc" => Value::str(self.dom.borrow().get_attr(idx, "src").unwrap_or_default()),
2211            // `img`/`script`/`link.crossOrigin`(丸ごと未対応だった)。この処理系
2212            // には実際の CORS リクエスト制御機構が無いため属性値の反映のみ行う。
2213            // 未指定時は仕様どおり `null`。
2214            "crossOrigin" => match self.dom.borrow().get_attr(idx, "crossorigin") {
2215                Some(v) => Value::str(v),
2216                None => Value::Null,
2217            },
2218            // `img`/`script`/`link.fetchPriority`(丸ごと未対応だった。既定値は
2219            // 仕様どおり `"auto"`)。
2220            "fetchPriority" => Value::str(
2221                self.dom.borrow().get_attr(idx, "fetchpriority").unwrap_or_else(|| "auto".into()),
2222            ),
2223            // `script`/`link.integrity`(丸ごと未対応だった。SRI 検証機構自体は
2224            // この処理系には無いため属性値の反映のみ行う)。
2225            "integrity" => Value::str(self.dom.borrow().get_attr(idx, "integrity").unwrap_or_default()),
2226            // `script.async`/`.defer`/`.noModule`(丸ごと未対応だった。`disabled`
2227            // 等と同じ真偽属性の反映のみを行う簡略実装)。
2228            "async" => Value::Bool(self.dom.borrow().has_attr(idx, "async")),
2229            "defer" => Value::Bool(self.dom.borrow().has_attr(idx, "defer")),
2230            "noModule" => Value::Bool(self.dom.borrow().has_attr(idx, "nomodule")),
2231            // `a`/`area`/`link.referrerPolicy`(丸ごと未対応だった)。この処理系は
2232            // 実際のリファラ送信制御機構を持たないため、`inputMode`等と同じ単純な
2233            // 文字列反映の簡略実装。
2234            "referrerPolicy" => {
2235                Value::str(self.dom.borrow().get_attr(idx, "referrerpolicy").unwrap_or_default())
2236            }
2237            // `link.as`(`rel="preload"`/`"prefetch"`と組み合わせて使う取得先
2238            // 種別ヒント。`referrerPolicy`と同じ単純な文字列反映だが丸ごと
2239            // 未対応だった。2026-07-18 発見・実装)。
2240            "as" => Value::str(self.dom.borrow().get_attr(idx, "as").unwrap_or_default()),
2241            // `link.imageSrcset`/`.imageSizes`(`rel="preload" as="image"`と
2242            // 組み合わせて使うレスポンシブ画像プリロードヒント。`link.as`
2243            // と同じ単純な文字列反映だが丸ごと未対応だった。2026-07-18
2244            // 発見・実装)。
2245            "imageSrcset" => Value::str(self.dom.borrow().get_attr(idx, "imagesrcset").unwrap_or_default()),
2246            "imageSizes" => Value::str(self.dom.borrow().get_attr(idx, "imagesizes").unwrap_or_default()),
2247            // `img`/`iframe.loading`(`lazy`/`eager`。既知の列挙値以外なら
2248            // 仕様上の既定値`"auto"`にフォールバックする必要があるが、
2249            // `fieldset.type`等と同じく単純な属性直結ゲッターに落ちて
2250            // 空文字列になっていた。2026-07-17 発見・実装)。この処理系には
2251            // 実際の遅延読み込み機構が無いため属性値の反映のみ行う。
2252            "loading" => {
2253                let v = self.dom.borrow().get_attr(idx, "loading").map(|s| s.to_lowercase());
2254                match v.as_deref() {
2255                    Some("lazy") => Value::str("lazy"),
2256                    Some("eager") => Value::str("eager"),
2257                    _ => Value::str("auto"),
2258                }
2259            }
2260            // `img.decoding`(`sync`/`async`/`auto`。既知の列挙値以外なら
2261            // 仕様上の既定値`"auto"`にフォールバックする必要があるが、同じ
2262            // バグパターンで空文字列になっていた。2026-07-17 発見・実装)。
2263            "decoding" => {
2264                let v = self.dom.borrow().get_attr(idx, "decoding").map(|s| s.to_lowercase());
2265                match v.as_deref() {
2266                    Some("sync") => Value::str("sync"),
2267                    Some("async") => Value::str("async"),
2268                    _ => Value::str("auto"),
2269                }
2270            }
2271            // `iframe.allow`(Permissions Policy 文字列。丸ごと未対応だった)。
2272            "allow" => Value::str(self.dom.borrow().get_attr(idx, "allow").unwrap_or_default()),
2273            // `iframe.srcdoc`(単純な文字列反映だが丸ごと未対応だった。
2274            // 実際にsrcdoc内容をレンダリングする機構は無いため属性値の
2275            // 反映のみ行う簡略実装。2026-07-17 発見・実装)。
2276            "srcdoc" => Value::str(self.dom.borrow().get_attr(idx, "srcdoc").unwrap_or_default()),
2277            // `iframe.sandbox`(`rel`属性と同じ空白区切りトークンの `DOMTokenList`。
2278            // 丸ごと未対応だった)。`relList` と同じ方針で読み取り専用のトークン
2279            // 配列にとどめる簡略実装(実際のサンドボックス隔離機構自体もこの
2280            // 処理系には無い)。
2281            "sandbox" => {
2282                let v = self.dom.borrow().get_attr(idx, "sandbox").unwrap_or_default();
2283                Value::Object(Obj::array(v.split_whitespace().map(Value::str).collect()))
2284            }
2285            // `element.popover`(`popover` グローバル属性の JS プロパティ版。
2286            // 丸ごと未対応だった。`showPopover()`等のメソッド自体は前サイクルで
2287            // 実装済みだが、属性の読み書き用 IDL プロパティが欠けていた)。
2288            // 属性が無ければ仕様どおり `null`。
2289            "popover" => match self.dom.borrow().get_attr(idx, "popover") {
2290                Some(v) => Value::str(v),
2291                None => Value::Null,
2292            },
2293            // `button.popoverTargetElement`(`popovertarget` idref 属性を実際の
2294            // 要素へ解決する IDL 属性。丸ごと未対応だった。`label.control`と同じ
2295            // id 解決パターンを再利用)。`popoverTargetAction` は単純な文字列
2296            // 反映(`inputMode`等と同じ簡略実装)。
2297            // `a`/`area`/`link.relList`(`rel` 属性を空白区切りトークンの
2298            // 集合として扱う `DOMTokenList`。丸ごと未対応だった)。`classList`
2299            // ほど頻出でないため、専用の `Host("relList:N")` プロキシ
2300            // (`add`/`remove`/`toggle`等のフル実装)までは実装せず、
2301            // `element.attributes`(`NamedNodeMap`の簡略化)と同じ方針で
2302            // トークンの配列という読み取り専用の簡略表現にとどめる
2303            // (`for...of`/`.includes()`/`.length` という最頻出パターンは
2304            // これで満たせる)。
2305            "relList" => {
2306                let rel = self.dom.borrow().get_attr(idx, "rel").unwrap_or_default();
2307                let tokens: Vec<Value> =
2308                    rel.split_whitespace().map(Value::str).collect();
2309                Value::Object(Obj::array(tokens))
2310            }
2311            // `element.part`(CSS Shadow Parts。`part`属性を空白区切り
2312            // トークンとして扱う`DOMTokenList`。丸ごと未対応だった)。
2313            // この処理系にはShadow DOM自体が無く`::part()`セレクタも
2314            // 意味を持たないが、`relList`/`sandbox`と同じ「読み取り専用
2315            // トークン配列」簡略実装としてプロパティ自体は提供する。
2316            // 2026-07-18 発見・実装)。
2317            "part" => {
2318                let v = self.dom.borrow().get_attr(idx, "part").unwrap_or_default();
2319                Value::Object(Obj::array(v.split_whitespace().map(Value::str).collect()))
2320            }
2321            // `link`/`script.blocking`(レンダリングブロッキング指定を空白
2322            // 区切りトークンとして扱う`DOMTokenList`。丸ごと未対応
2323            // だった)。`part`/`relList`/`sandbox`と同じ読み取り専用
2324            // トークン配列の簡略実装。2026-07-18 発見・実装)。
2325            "blocking" => {
2326                let v = self.dom.borrow().get_attr(idx, "blocking").unwrap_or_default();
2327                Value::Object(Obj::array(v.split_whitespace().map(Value::str).collect()))
2328            }
2329            // `input.list`(`list` idref 属性から実際の `<datalist>` 要素を
2330            // 解決する IDL 属性。丸ごと未対応だった。`popoverTargetElement` と
2331            // 同じ id 解決パターンを再利用。参照先が `<datalist>` でなければ
2332            // 仕様どおり `null`)。
2333            "list" => {
2334                let dom = self.dom.borrow();
2335                match dom.get_attr(idx, "list").filter(|f| !f.is_empty()) {
2336                    Some(target_id) => match dom.get_element_by_id(&target_id) {
2337                        Some(ti) if dom.nodes.get(ti).map(|n| n.tag == "datalist").unwrap_or(false) => {
2338                            Value::Object(Obj::dom(ti))
2339                        }
2340                        _ => Value::Null,
2341                    },
2342                    None => Value::Null,
2343                }
2344            }
2345            "popoverTargetElement" => {
2346                let dom = self.dom.borrow();
2347                match dom.get_attr(idx, "popovertarget").filter(|f| !f.is_empty()) {
2348                    Some(target_id) => match dom.get_element_by_id(&target_id) {
2349                        Some(ti) => Value::Object(Obj::dom(ti)),
2350                        None => Value::Null,
2351                    },
2352                    None => Value::Null,
2353                }
2354            }
2355            "popoverTargetAction" => {
2356                Value::str(self.dom.borrow().get_attr(idx, "popovertargetaction").unwrap_or_default())
2357            }
2358            // `button.command`/`.commandForElement`(Invoker Commands API。
2359            // `<button command="show-modal" commandfor="my-dialog">`という
2360            // 宣言的UIパターンが丸ごと未対応だった。`popoverTargetElement`と
2361            // 全く同じidref解決パターンを再利用する(`commandfor`属性→実際の
2362            // 要素)。クリック時に実際に`CommandEvent`を発火し`show-modal`/
2363            // `close`/`toggle-popover`等の組込み動作を実行する経路は
2364            // `render.rs`側のクリックディスパッチへの新規配線が必要で改修
2365            // 範囲が広いため、本サイクルではプロパティ反映のみ実装する
2366            // (詳細はwalkthrough.md参照)。2026-07-18 発見・実装)。
2367            "command" => Value::str(self.dom.borrow().get_attr(idx, "command").unwrap_or_default()),
2368            "commandForElement" => {
2369                let dom = self.dom.borrow();
2370                match dom.get_attr(idx, "commandfor").filter(|f| !f.is_empty()) {
2371                    Some(target_id) => match dom.get_element_by_id(&target_id) {
2372                        Some(ti) => Value::Object(Obj::dom(ti)),
2373                        None => Value::Null,
2374                    },
2375                    None => Value::Null,
2376                }
2377            }
2378            "style" => Value::Object(Obj::host(&format!("style:{}", idx))),
2379            "classList" => Value::Object(Obj::host(&format!("classList:{}", idx))),
2380            "dataset" => Value::Object(Obj::host(&format!("dataset:{}", idx))),
2381            "addEventListener" => Value::Object(Obj::native(
2382                "addEventListener",
2383                super::super::builtins::dom_add_event_listener,
2384            )),
2385            "removeEventListener" => Value::Object(Obj::native(
2386                "removeEventListener",
2387                super::super::builtins::dom_remove_event_listener,
2388            )),
2389            "dispatchEvent" => Value::Object(Obj::native(
2390                "dispatchEvent",
2391                super::super::builtins::dom_dispatch_event,
2392            )),
2393            // `node.hasChildNodes()`(丸ごと未対応だった。2026-07-15 発見・実装)。
2394            "hasChildNodes" => Value::Object(Obj::native(
2395                "hasChildNodes",
2396                super::super::builtins::dom_has_child_nodes,
2397            )),
2398            "getAttribute" => Value::Object(Obj::native(
2399                "getAttribute",
2400                super::super::builtins::dom_get_attribute,
2401            )),
2402            // `getAttributeNode`/`setAttributeNode`/`removeAttributeNode`
2403            // (丸ごと未対応だった。`getAttribute` より古い/低レベルな `Attr`
2404            // ノード経由の定番イディオム。2026-07-14 発見・実装)。
2405            "getAttributeNode" => Value::Object(Obj::native(
2406                "getAttributeNode",
2407                super::super::builtins::dom_get_attribute_node,
2408            )),
2409            "getAttributeNodeNS" => Value::Object(Obj::native(
2410                "getAttributeNodeNS",
2411                super::super::builtins::dom_get_attribute_node_ns,
2412            )),
2413            "setAttributeNode" => Value::Object(Obj::native(
2414                "setAttributeNode",
2415                super::super::builtins::dom_set_attribute_node,
2416            )),
2417            "removeAttributeNode" => Value::Object(Obj::native(
2418                "removeAttributeNode",
2419                super::super::builtins::dom_remove_attribute_node,
2420            )),
2421            "setAttribute" => Value::Object(Obj::native(
2422                "setAttribute",
2423                super::super::builtins::dom_set_attribute,
2424            )),
2425            "hasAttribute" => Value::Object(Obj::native(
2426                "hasAttribute",
2427                super::super::builtins::dom_has_attribute,
2428            )),
2429            "removeAttribute" => Value::Object(Obj::native(
2430                "removeAttribute",
2431                super::super::builtins::dom_remove_attribute,
2432            )),
2433            "getAttributeNS" => Value::Object(Obj::native(
2434                "getAttributeNS",
2435                super::super::builtins::dom_get_attribute_ns,
2436            )),
2437            "setAttributeNS" => Value::Object(Obj::native(
2438                "setAttributeNS",
2439                super::super::builtins::dom_set_attribute_ns,
2440            )),
2441            "hasAttributeNS" => Value::Object(Obj::native(
2442                "hasAttributeNS",
2443                super::super::builtins::dom_has_attribute_ns,
2444            )),
2445            "removeAttributeNS" => Value::Object(Obj::native(
2446                "removeAttributeNS",
2447                super::super::builtins::dom_remove_attribute_ns,
2448            )),
2449            "getAttributeNames" => Value::Object(Obj::native(
2450                "getAttributeNames",
2451                super::super::builtins::dom_get_attribute_names,
2452            )),
2453            "hasAttributes" => Value::Object(Obj::native(
2454                "hasAttributes",
2455                super::super::builtins::dom_has_attributes,
2456            )),
2457            "normalize" => Value::Object(Obj::native("normalize", super::super::builtins::dom_normalize)),
2458            "isSameNode" => Value::Object(Obj::native("isSameNode", super::super::builtins::dom_is_same_node)),
2459            "isEqualNode" => {
2460                Value::Object(Obj::native("isEqualNode", super::super::builtins::dom_is_equal_node))
2461            }
2462            "matches" | "webkitMatchesSelector" => {
2463                Value::Object(Obj::native("matches", super::super::builtins::dom_matches))
2464            }
2465            "closest" => Value::Object(Obj::native("closest", super::super::builtins::dom_closest)),
2466            "getBoundingClientRect" => Value::Object(Obj::native(
2467                "getBoundingClientRect",
2468                super::super::builtins::dom_get_bounding_client_rect,
2469            )),
2470            "getClientRects" => Value::Object(Obj::native(
2471                "getClientRects",
2472                super::super::builtins::dom_get_client_rects,
2473            )),
2474            "setPointerCapture" => Value::Object(Obj::native(
2475                "setPointerCapture",
2476                super::super::builtins::dom_set_pointer_capture,
2477            )),
2478            "releasePointerCapture" => Value::Object(Obj::native(
2479                "releasePointerCapture",
2480                super::super::builtins::dom_release_pointer_capture,
2481            )),
2482            "hasPointerCapture" => Value::Object(Obj::native(
2483                "hasPointerCapture",
2484                super::super::builtins::dom_has_pointer_capture,
2485            )),
2486            "offsetWidth" => {
2487                Value::Number(self.dom.borrow().get_rect(idx).map(|r| r.2).unwrap_or(0) as f64)
2488            }
2489            "clientWidth" => {
2490                let dom = self.dom.borrow();
2491                let w = dom.get_rect(idx).map(|r| r.2).unwrap_or(0);
2492                let borders = dom.get_border_widths(idx);
2493                Value::Number((w - borders.3 - borders.1).max(0) as f64)
2494            }
2495            "offsetHeight" => {
2496                Value::Number(self.dom.borrow().get_rect(idx).map(|r| r.3).unwrap_or(0) as f64)
2497            }
2498            "clientHeight" => {
2499                let dom = self.dom.borrow();
2500                let h = dom.get_rect(idx).map(|r| r.3).unwrap_or(0);
2501                let borders = dom.get_border_widths(idx);
2502                Value::Number((h - borders.0 - borders.2).max(0) as f64)
2503            }
2504            "offsetLeft" => {
2505                Value::Number(self.dom.borrow().get_rect(idx).map(|r| r.0).unwrap_or(0) as f64)
2506            }
2507            "offsetTop" => {
2508                Value::Number(self.dom.borrow().get_rect(idx).map(|r| r.1).unwrap_or(0) as f64)
2509            }
2510            // `element.offsetParent`(HTML5。丸ごと未対応だった。`offsetLeft`/
2511            // `offsetTop` と組み合わせて祖先からの相対位置を計算する定番
2512            // パターンで使われる)。
2513            "offsetParent" => match self.dom.borrow().offset_parent(idx) {
2514                Some(pi) => Value::Object(Obj::dom(pi)),
2515                None => Value::Null,
2516            },
2517            "scrollWidth" => {
2518                let dom = self.dom.borrow();
2519                let v = dom.scroll_widths.get(&idx).copied().unwrap_or_else(|| {
2520                    dom.get_rect(idx).map(|r| r.2).unwrap_or(0)
2521                });
2522                Value::Number(v as f64)
2523            }
2524            "scrollHeight" => {
2525                let dom = self.dom.borrow();
2526                let v = dom.scroll_heights.get(&idx).copied().unwrap_or_else(|| {
2527                    dom.get_rect(idx).map(|r| r.3).unwrap_or(0)
2528                });
2529                Value::Number(v as f64)
2530            }
2531            "scrollTop" => {
2532                let v = self.dom.borrow().scroll_tops.get(&idx).copied().unwrap_or(0);
2533                Value::Number(v as f64)
2534            }
2535            "scrollLeft" => {
2536                let v = self.dom.borrow().scroll_lefts.get(&idx).copied().unwrap_or(0);
2537                Value::Number(v as f64)
2538            }
2539            "clientLeft" => {
2540                let borders = self.dom.borrow().get_border_widths(idx);
2541                Value::Number(borders.3 as f64)
2542            }
2543            "clientTop" => {
2544                let borders = self.dom.borrow().get_border_widths(idx);
2545                Value::Number(borders.0 as f64)
2546            }
2547            // `nodeType` が常に `1`(ELEMENT_NODE)固定で、テキストノードでも
2548            // `3`(TEXT_NODE)を返さないバグだった。`nodeName` も同様にテキストノードで
2549            // 仕様どおりの `"#text"` ではなく空文字列(`tag` がテキストノードでは
2550            // 空のため)になっていた。
2551            "nodeType" => {
2552                let tag = self.dom.borrow().nodes.get(idx).map(|n| n.tag.clone()).unwrap_or_default();
2553                let is_text = self.dom.borrow().nodes.get(idx).map(|n| n.is_text).unwrap_or(false);
2554                let num = if is_text {
2555                    3.0
2556                } else if tag == "#comment" {
2557                    8.0
2558                } else if tag == "#document-fragment" || tag == "#fragment" {
2559                    11.0
2560                } else {
2561                    1.0
2562                };
2563                Value::Number(num)
2564            }
2565            "nodeName" => {
2566                let dom = self.dom.borrow();
2567                let name = match dom.nodes.get(idx) {
2568                    Some(n) if n.is_text => String::from("#text"),
2569                    Some(n) if n.tag == "#comment" => String::from("#comment"),
2570                    Some(n) if n.tag == "#document-fragment" || n.tag == "#fragment" => String::from("#document-fragment"),
2571                    Some(n) => n.tag.to_uppercase(),
2572                    None => String::new(),
2573                };
2574                Value::str(name)
2575            }
2576            // `node.nodeValue`(テキストノードでは自身のテキスト、要素ノードでは
2577            // 仕様どおり `null`)が `nodeType`/`nodeName` と同じ理由で丸ごと未対応
2578            // だった。`childNodes` 経由で得たテキストノードの中身を読む定番パターンで
2579            // 使われる。
2580            "nodeValue" => {
2581                let dom = self.dom.borrow();
2582                match dom.nodes.get(idx) {
2583                    Some(n) if n.is_text || n.tag == "#comment" => Value::str(dom.get_text_content(idx)),
2584                    _ => Value::Null,
2585                }
2586            }
2587            // `CharacterData.data`/`.length`(テキストノードの生データ
2588            // アクセサ。丸ごと未対応だった。`nodeValue`と同じ値を指す
2589            // エイリアスだが、`textNode.data`単体で読み書きする定番
2590            // イディオムには`nodeValue`だけでは対応できていなかった。
2591            // 2026-07-16 発見)。文字数は他の文字列系メソッド群
2592            // (`str_slice`/`str_substring`)と同じ char 単位の簡略実装。
2593            "data" if self.dom.borrow().nodes.get(idx).map(|n| n.is_text || n.tag == "#comment").unwrap_or(false) => {
2594                Value::str(self.dom.borrow().get_text_content(idx))
2595            }
2596            "length" if self.dom.borrow().nodes.get(idx).map(|n| n.is_text || n.tag == "#comment").unwrap_or(false) => {
2597                Value::Number(self.dom.borrow().get_text_content(idx).chars().count() as f64)
2598            }
2599            // `CharacterData.appendData`/`.deleteData`/`.insertData`/
2600            // `.replaceData`/`.substringData`(丸ごと未対応だった。
2601            // 2026-07-16 発見)。テキストノード以外では no-op/空文字列を
2602            // 返す誠実な簡略実装。
2603            "appendData" if self.dom.borrow().nodes.get(idx).map(|n| n.is_text || n.tag == "#comment").unwrap_or(false) => {
2604                Value::Object(Obj::native("appendData", super::super::builtins::dom_char_data_append))
2605            }
2606            "deleteData" if self.dom.borrow().nodes.get(idx).map(|n| n.is_text || n.tag == "#comment").unwrap_or(false) => {
2607                Value::Object(Obj::native("deleteData", super::super::builtins::dom_char_data_delete))
2608            }
2609            "insertData" if self.dom.borrow().nodes.get(idx).map(|n| n.is_text || n.tag == "#comment").unwrap_or(false) => {
2610                Value::Object(Obj::native("insertData", super::super::builtins::dom_char_data_insert))
2611            }
2612            "replaceData" if self.dom.borrow().nodes.get(idx).map(|n| n.is_text || n.tag == "#comment").unwrap_or(false) => {
2613                Value::Object(Obj::native("replaceData", super::super::builtins::dom_char_data_replace))
2614            }
2615            "substringData" if self.dom.borrow().nodes.get(idx).map(|n| n.is_text || n.tag == "#comment").unwrap_or(false) => {
2616                Value::Object(Obj::native("substringData", super::super::builtins::dom_char_data_substring))
2617            }
2618            // `Text.splitText(offset)`(丸ごと未対応だった。2026-07-16 発見・実装)。
2619            "splitText" if self.dom.borrow().nodes.get(idx).map(|n| n.is_text).unwrap_or(false) => {
2620                Value::Object(Obj::native("splitText", super::super::builtins::dom_split_text))
2621            }
2622            // `Text.wholeText`(丸ごと未対応だった。`splitText`と対をなす
2623            // 逆操作の読み取りプロパティ。2026-07-16 発見・実装)。連続する
2624            // テキストノード兄弟(`splitText`で分割した直後等)の`data`を
2625            // 全て連結した文字列を返す。
2626            "wholeText" if self.dom.borrow().nodes.get(idx).map(|n| n.is_text).unwrap_or(false) => {
2627                let dom = self.dom.borrow();
2628                let parent = dom.nodes.get(idx).and_then(|n| n.parent);
2629                let out = match parent {
2630                    Some(p) => {
2631                        let is_text = |c: usize| dom.nodes.get(c).map(|n| n.is_text).unwrap_or(false);
2632                        let children = &dom.nodes[p].children;
2633                        let pos = children.iter().position(|&c| c == idx).unwrap_or(0);
2634                        let mut start = pos;
2635                        while start > 0 && is_text(children[start - 1]) {
2636                            start -= 1;
2637                        }
2638                        let mut end = pos;
2639                        while end + 1 < children.len() && is_text(children[end + 1]) {
2640                            end += 1;
2641                        }
2642                        children[start..=end].iter().map(|&c| dom.get_text_content(c)).collect()
2643                    }
2644                    None => dom.get_text_content(idx),
2645                };
2646                Value::str(out)
2647            }
2648            // `childNodes`/`firstChild`/`lastChild`/`nextSibling`/`previousSibling`
2649            // (テキストノードも含む Node レベルの走査 API)が丸ごと未対応だった。
2650            // `children`/`firstElementChild`/.../`nextElementSibling` 等(要素のみに
2651            // 絞る Element レベルの API)は既に対応済みだったが、対をなす Node 版が
2652            // 漏れていた。
2653            "childNodes" => {
2654                let is_template = self.dom.borrow().nodes.get(idx).map(|n| n.tag == "template").unwrap_or(false);
2655                let kids = if is_template {
2656                    Vec::new()
2657                } else {
2658                    self.dom
2659                        .borrow()
2660                        .nodes
2661                        .get(idx)
2662                        .map(|n| n.children.clone())
2663                        .unwrap_or_default()
2664                };
2665                Value::Object(Obj::array(
2666                    kids.into_iter().map(|i| Value::Object(Obj::dom(i))).collect(),
2667                ))
2668            }
2669            "firstChild" => {
2670                let is_template = self.dom.borrow().nodes.get(idx).map(|n| n.tag == "template").unwrap_or(false);
2671                let first = if is_template {
2672                    None
2673                } else {
2674                    self.dom
2675                        .borrow()
2676                        .nodes
2677                        .get(idx)
2678                        .and_then(|n| n.children.first().copied())
2679                };
2680                match first {
2681                    Some(i) => Value::Object(Obj::dom(i)),
2682                    None => Value::Null,
2683                }
2684            }
2685            "lastChild" => {
2686                let is_template = self.dom.borrow().nodes.get(idx).map(|n| n.tag == "template").unwrap_or(false);
2687                let last = if is_template {
2688                    None
2689                } else {
2690                    self.dom
2691                        .borrow()
2692                        .nodes
2693                        .get(idx)
2694                        .and_then(|n| n.children.last().copied())
2695                };
2696                match last {
2697                    Some(i) => Value::Object(Obj::dom(i)),
2698                    None => Value::Null,
2699                }
2700            }
2701            "nextSibling" | "previousSibling" => {
2702                let dom = self.dom.borrow();
2703                let sib = dom.nodes.get(idx).and_then(|n| n.parent).and_then(|p| {
2704                    let kids = &dom.nodes.get(p)?.children;
2705                    let pos = kids.iter().position(|&c| c == idx)?;
2706                    if key == "nextSibling" {
2707                        kids.get(pos + 1).copied()
2708                    } else if pos > 0 {
2709                        kids.get(pos - 1).copied()
2710                    } else {
2711                        None
2712                    }
2713                });
2714                match sib {
2715                    Some(i) => Value::Object(Obj::dom(i)),
2716                    None => Value::Null,
2717                }
2718            }
2719            "hidden" => Value::Bool(self.dom.borrow().has_attr(idx, "hidden")),
2720            "querySelector" => Value::Object(Obj::native(
2721                "querySelector",
2722                super::super::builtins::dom_element_query_selector,
2723            )),
2724            "querySelectorAll" => Value::Object(Obj::native(
2725                "querySelectorAll",
2726                super::super::builtins::dom_element_query_selector_all,
2727            )),
2728            // `document.getElementsByClassName`/`.getElementsByTagName` は対応済み
2729            // だったが、要素インスタンス版(自身の子孫のみに絞り込む)が丸ごと
2730            // 未対応だった。
2731            "getElementsByClassName" => Value::Object(Obj::native(
2732                "getElementsByClassName",
2733                super::super::builtins::dom_element_get_elements_by_class_name,
2734            )),
2735            "getElementsByTagName" => Value::Object(Obj::native(
2736                "getElementsByTagName",
2737                super::super::builtins::dom_element_get_elements_by_tag_name,
2738            )),
2739            "getElementsByTagNameNS" => Value::Object(Obj::native(
2740                "getElementsByTagNameNS",
2741                super::super::builtins::dom_element_get_elements_by_tag_name_ns,
2742            )),
2743            "appendChild" => Value::Object(Obj::native(
2744                "appendChild",
2745                super::super::builtins::dom_append_child,
2746            )),
2747            "insertBefore" => Value::Object(Obj::native(
2748                "insertBefore",
2749                super::super::builtins::dom_insert_before,
2750            )),
2751            // `parent.moveBefore(movedNode, referenceNode)`(丸ごと未対応
2752            // だった。詳細は`dom_move_before`参照。2026-07-18 発見・実装)。
2753            "moveBefore" => Value::Object(Obj::native(
2754                "moveBefore",
2755                super::super::builtins::dom_move_before,
2756            )),
2757            "replaceChild" => Value::Object(Obj::native(
2758                "replaceChild",
2759                super::super::builtins::dom_replace_child,
2760            )),
2761            "cloneNode" => Value::Object(Obj::native("cloneNode", super::super::builtins::dom_clone_node)),
2762            "contains" => Value::Object(Obj::native("contains", super::super::builtins::dom_contains)),
2763            "compareDocumentPosition" => Value::Object(Obj::native(
2764                "compareDocumentPosition",
2765                super::super::builtins::dom_compare_document_position,
2766            )),
2767            "getRootNode" => Value::Object(Obj::native(
2768                "getRootNode",
2769                super::super::builtins::dom_get_root_node,
2770            )),
2771            // `lookupNamespaceURI`/`lookupPrefix`/`isDefaultNamespace`(丸ごと
2772            // 未対応だった。この処理系は XML 名前空間を一切モデル化していない
2773            // ため常に「名前空間なし」を返す簡略実装。詳細は builtins.rs 参照。
2774            "lookupNamespaceURI" => Value::Object(Obj::native(
2775                "lookupNamespaceURI",
2776                super::super::builtins::dom_lookup_namespace_uri,
2777            )),
2778            "lookupPrefix" => Value::Object(Obj::native(
2779                "lookupPrefix",
2780                super::super::builtins::dom_lookup_prefix,
2781            )),
2782            // `element.checkVisibility()`(DOM 標準。丸ごと未対応だった)。
2783            "checkVisibility" => Value::Object(Obj::native(
2784                "checkVisibility",
2785                super::super::builtins::dom_check_visibility,
2786            )),
2787            "isDefaultNamespace" => Value::Object(Obj::native(
2788                "isDefaultNamespace",
2789                super::super::builtins::dom_is_default_namespace,
2790            )),
2791            // `element.requestFullscreen()`(Fullscreen API + ベンダープレフィックス別名)。
2792            // 詳細は `builtins::dom_request_fullscreen` 参照。
2793            "requestFullscreen"
2794            | "webkitRequestFullscreen"
2795            | "webkitRequestFullScreen"
2796            | "mozRequestFullScreen"
2797            | "msRequestFullscreen" => Value::Object(Obj::native(
2798                "requestFullscreen",
2799                super::super::builtins::dom_request_fullscreen,
2800            )),
2801            // `element.requestPointerLock()`(Pointer Lock API。丸ごと未対応
2802            // だった)。詳細は `builtins::dom_request_pointer_lock` 参照。
2803            "requestPointerLock" => Value::Object(Obj::native(
2804                "requestPointerLock",
2805                super::super::builtins::dom_request_pointer_lock,
2806            )),
2807            // `<video>.requestPictureInPicture()`(Picture-in-Picture API。丸ごと
2808            // 未対応だった)。詳細は `builtins::dom_request_picture_in_picture` 参照。
2809            "requestPictureInPicture" => Value::Object(Obj::native(
2810                "requestPictureInPicture",
2811                super::super::builtins::dom_request_picture_in_picture,
2812            )),
2813            "append" => Value::Object(Obj::native("append", super::super::builtins::dom_append)),
2814            "prepend" => Value::Object(Obj::native("prepend", super::super::builtins::dom_prepend)),
2815            // `element.replaceChildren(...nodes)`(`innerHTML=''; append(...)` の
2816            // 1行版として使われる定番イディオム)が丸ごと未対応だった。
2817            "replaceChildren" => Value::Object(Obj::native(
2818                "replaceChildren",
2819                super::super::builtins::dom_replace_children,
2820            )),
2821            "before" => Value::Object(Obj::native("before", super::super::builtins::dom_before)),
2822            "after" => Value::Object(Obj::native("after", super::super::builtins::dom_after)),
2823            "replaceWith" => Value::Object(Obj::native(
2824                "replaceWith",
2825                super::super::builtins::dom_replace_with,
2826            )),
2827            "insertAdjacentHTML" => Value::Object(Obj::native(
2828                "insertAdjacentHTML",
2829                super::super::builtins::dom_insert_adjacent_html,
2830            )),
2831            // `element.getHTML()`/`.setHTMLUnsafe()`(丸ごと未対応だった。
2832            // 詳細は`dom_get_html`/`dom_set_html_unsafe`参照。2026-07-18
2833            // 発見・実装)。
2834            "getHTML" => Value::Object(Obj::native("getHTML", super::super::builtins::dom_get_html)),
2835            "setHTMLUnsafe" => Value::Object(Obj::native(
2836                "setHTMLUnsafe",
2837                super::super::builtins::dom_set_html_unsafe,
2838            )),
2839            "insertAdjacentElement" => Value::Object(Obj::native(
2840                "insertAdjacentElement",
2841                super::super::builtins::dom_insert_adjacent_element,
2842            )),
2843            "insertAdjacentText" => Value::Object(Obj::native(
2844                "insertAdjacentText",
2845                super::super::builtins::dom_insert_adjacent_text,
2846            )),
2847            "toggleAttribute" => Value::Object(Obj::native(
2848                "toggleAttribute",
2849                super::super::builtins::dom_toggle_attribute,
2850            )),
2851            // `dialog.show()`/`.showModal()`/`.close(returnValue?)`(`<dialog>` が丸ごと
2852            // 未対応だった)。`open` 属性の付け外しのみを行う簡略実装(`::backdrop` 描画・
2853            // モーダルフォーカストラップ・ESC キーでの自動 close は対象外)。
2854            "show" => Value::Object(Obj::native("show", super::super::builtins::dom_dialog_show)),
2855            "showModal" => {
2856                Value::Object(Obj::native("showModal", super::super::builtins::dom_dialog_show_modal))
2857            }
2858            "close" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "dialog").unwrap_or(false) => {
2859                Value::Object(Obj::native("close", super::super::builtins::dom_dialog_close))
2860            }
2861            // `dialog.requestClose(returnValue?)`(`.close()`と違い、閉じる
2862            // 直前に取消可能な`cancel`イベントを発火する。丸ごと未対応
2863            // だった。2026-07-18 発見・実装)。
2864            "requestClose"
2865                if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "dialog").unwrap_or(false) =>
2866            {
2867                Value::Object(Obj::native("requestClose", super::super::builtins::dom_dialog_request_close))
2868            }
2869            // Popover API(`popover` グローバル属性。丸ごと未対応だった)。`dialog`
2870            // の `show`/`close` と同じ「内部専用属性の付け外しのみ」の簡略実装。
2871            "showPopover" => Value::Object(Obj::native(
2872                "showPopover",
2873                super::super::builtins::dom_show_popover,
2874            )),
2875            "hidePopover" => Value::Object(Obj::native(
2876                "hidePopover",
2877                super::super::builtins::dom_hide_popover,
2878            )),
2879            "togglePopover" => Value::Object(Obj::native(
2880                "togglePopover",
2881                super::super::builtins::dom_toggle_popover,
2882            )),
2883            "removeChild" => Value::Object(Obj::native(
2884                "removeChild",
2885                super::super::builtins::dom_remove_child,
2886            )),
2887            "remove" => Value::Object(Obj::native("remove", super::super::builtins::dom_remove)),
2888            // `node.ownerDocument`(`element.ownerDocument.createElement(...)` という
2889            // 定番イディオムや単純な機能検出で使われる)が丸ごと未対応だった。この
2890            // エンジンには document は常に1つしか無いため、グローバルの `document`
2891            // オブジェクトをそのまま返せばよい。
2892            "ownerDocument" => match self.global.borrow().vars.get("document") {
2893                Some(d) => d.clone(),
2894                None => Value::Null,
2895            },
2896            // `node.isConnected`(現在 document に接続されているかの真偽値。
2897            // `createElement` 直後や `remove()` 済みのノードで `false` になる。
2898            // click-outside 判定等で `contains`/`querySelector` と並んでよく使われる)が
2899            // 丸ごと未対応だった。既存の `is_attached`(`getElementById` 等が内部で
2900            // 使っていた到達可能性チェック)をそのまま公開すれば実装できる。
2901            "isConnected" => Value::Bool(self.dom.borrow().is_attached(idx)),
2902            // ナビゲーション系プロパティ(メソッドではなく値)。
2903            "parentElement" | "parentNode" => {
2904                let p = self.dom.borrow().nodes.get(idx).and_then(|n| n.parent);
2905                match p {
2906                    Some(pi) => Value::Object(Obj::dom(pi)),
2907                    None => Value::Null,
2908                }
2909            }
2910            "children" => {
2911                let is_template = self.dom.borrow().nodes.get(idx).map(|n| n.tag == "template").unwrap_or(false);
2912                let kids = if is_template { Vec::new() } else { self.dom.borrow().element_children(idx) };
2913                super::super::builtins::make_html_collection(self, kids)
2914            }
2915            "childElementCount" => {
2916                let is_template = self.dom.borrow().nodes.get(idx).map(|n| n.tag == "template").unwrap_or(false);
2917                let count = if is_template { 0 } else { self.dom.borrow().element_children(idx).len() };
2918                Value::Number(count as f64)
2919            }
2920            "firstElementChild" => {
2921                let is_template = self.dom.borrow().nodes.get(idx).map(|n| n.tag == "template").unwrap_or(false);
2922                let first = if is_template { None } else { self.dom.borrow().element_children(idx).first().copied() };
2923                match first {
2924                    Some(i) => Value::Object(Obj::dom(i)),
2925                    None => Value::Null,
2926                }
2927            }
2928            "lastElementChild" => {
2929                let is_template = self.dom.borrow().nodes.get(idx).map(|n| n.tag == "template").unwrap_or(false);
2930                let last = if is_template { None } else { self.dom.borrow().element_children(idx).last().copied() };
2931                match last {
2932                    Some(i) => Value::Object(Obj::dom(i)),
2933                    None => Value::Null,
2934                }
2935            }
2936            "nextElementSibling" | "previousElementSibling" => {
2937                let dom = self.dom.borrow();
2938                let sib = dom.nodes.get(idx).and_then(|n| n.parent).and_then(|p| {
2939                    let kids = dom.element_children(p);
2940                    let pos = kids.iter().position(|&c| c == idx);
2941                    match pos {
2942                        Some(pi) if key == "nextElementSibling" => kids.get(pi + 1).copied(),
2943                        Some(pi) if pi > 0 => kids.get(pi - 1).copied(),
2944                        _ => None,
2945                    }
2946                });
2947                match sib {
2948                    Some(i) => Value::Object(Obj::dom(i)),
2949                    None => Value::Null,
2950                }
2951            }
2952            "click" => Value::Object(Obj::native("click", super::super::builtins::dom_click)),
2953            // `form.requestSubmit(submitter?)`/`form.submit()`(HTML5)が丸ごと
2954            // 未対応だった(`submit`type ボタンのクリック経由の送信は既に
2955            // `web_engine/render.rs` から動いていたが、JS から直接呼ぶ経路が
2956            // 無かった)。`requestSubmit` は仕様どおり `submit` イベントを発火
2957            // (`preventDefault()` で中止可能)。`submit` は仕様上イベント発火も
2958            // バリデーションもバイパスして直接送信するが、この処理系には実際の
2959            // フォーム送信/ページ遷移パイプライン自体が無いため、`focus`/`blur`/
2960            // `scrollIntoView` と同じ無害な no-op とする。
2961            "requestSubmit" => {
2962                Value::Object(Obj::native("requestSubmit", super::super::builtins::dom_request_submit))
2963            }
2964            "submit" => Value::Object(Obj::native("domNoop", |_, _, _| Ok(Value::Undefined))),
2965            "reset" => Value::Object(Obj::native("reset", super::super::builtins::dom_form_reset)),
2966            // `input.showPicker()`(HTML5。`<input type="date/color/file">` 等の
2967            // ネイティブピッカー UI を開く)が丸ごと未対応だった。この処理系には
2968            // そもそもネイティブピッカー UI 自体が無いため、`focus`/`blur`/
2969            // `scrollIntoView`/`submit` と同じ無害な no-op とする(仕様上の
2970            // `InvalidStateError`/`NotAllowedError` 例外条件はチェックしない
2971            // 簡略実装)。
2972            "showPicker" => Value::Object(Obj::native("domNoop", |_, _, _| Ok(Value::Undefined))),
2973            // `element.getAnimations()`(丸ごと未対応だった)。既存の
2974            // `.animate()` は呼び出しごとに使い捨ての no-op `Animation`
2975            // オブジェクトを返すだけで、要素側に一切保持していないため、
2976            // 実行中アニメーションが無いという誠実な空配列を返す簡略実装。
2977            "getAnimations" => Value::Object(Obj::native(
2978                "getAnimations",
2979                |_, _, _| Ok(Value::Object(Obj::array(alloc::vec![]))),
2980            )),
2981            // `input.stepUp(n?)`/`.stepDown(n?)`(HTML5)が丸ごと未対応だった
2982            // (詳細は builtins.rs の `dom_step_by` 実装コメント参照)。
2983            "stepUp" => Value::Object(Obj::native("stepUp", super::super::builtins::dom_step_up)),
2984            "stepDown" => Value::Object(Obj::native("stepDown", super::super::builtins::dom_step_down)),
2985            // `element.scrollTo(x,y)`/`.scrollBy(x,y)`/`.scroll(x,y)`(オブジェクト引数 `{top,left}`
2986            // 形式も含む)。`scroll` は W3C 仕様で `scrollTo` の同義エイリアス。
2987            "scroll" => Value::Object(Obj::native("scroll", super::super::builtins::dom_scroll_to)),
2988            "scrollTo" => Value::Object(Obj::native("scrollTo", super::super::builtins::dom_scroll_to)),
2989            "scrollBy" => Value::Object(Obj::native("scrollBy", super::super::builtins::dom_scroll_by)),
2990            // `scrollIntoView` は既存の`window.scrollTo`と同じ`pending_scroll_abs_y`
2991            // 機構を再利用して実装済み(2026-07-22。詳細は`dom_scroll_into_view`
2992            // のdocコメント参照)。
2993            "scrollIntoView" => Value::Object(Obj::native("scrollIntoView", super::super::builtins::dom_scroll_into_view)),
2994            "scrollIntoViewIfNeeded" => Value::Object(Obj::native("scrollIntoViewIfNeeded", super::super::builtins::dom_scroll_into_view)),
2995            // `element.focus()`/`.blur()`(HTML5。丸ごと未対応だった。以前は
2996            // `scrollIntoView` と同じ no-op 扱いで、`document.activeElement` が
2997            // 常に `null` 固定になる原因だった。詳細は `dom_focus`/`dom_blur` 参照)。
2998            "focus" => Value::Object(Obj::native("focus", super::super::builtins::dom_focus)),
2999            "blur" => Value::Object(Obj::native("blur", super::super::builtins::dom_blur)),
3000            // `<audio>`/`<video>` の `.play()`/`.pause()`/`.load()`/`.canPlayType()`
3001            // (HTMLMediaElement。丸ごと未対応だった。呼ぶだけで
3002            // `TypeError: not a function` になっていた)。実際の音声/映像出力までは
3003            // 配線しない無害な簡略実装(`focus`/`blur` と同種の割り切り)だが、
3004            // `.play()` は仕様どおり Promise を返す必要があるため専用の関数とする
3005            // (`.then()`/`await` する定番パターンで別種のエラーになるのを防ぐ)。
3006            "play" => Value::Object(Obj::native("play", super::super::builtins::dom_media_play)),
3007            "pause" => Value::Object(Obj::native("pause", super::super::builtins::dom_media_pause)),
3008            "load" => Value::Object(Obj::native("domNoop", |_, _, _| Ok(Value::Undefined))),
3009            // `canPlayType(type)`(W3C HTML5 §4.8.14.4)
3010            // AtmOS のメディアデコーダ機能に基づき "probably" / "maybe" / "" を返す。
3011            "canPlayType" => {
3012                Value::Object(Obj::native("canPlayType", |_, _, args| {
3013                    let type_str = args.first().map(|v| v.to_js_string()).unwrap_or_default();
3014                    let res = crate::os_lib::web_engine::media_controls::can_play_type(&type_str);
3015                    Ok(Value::str(res))
3016                }))
3017            }
3018            // Element.animate() → no-op Animation オブジェクトを返す
3019            "animate" => Value::Object(Obj::native("animate", |_, _, _| {
3020                let anim = Obj::plain();
3021                {
3022                    let mut b = anim.borrow_mut();
3023                    b.props.insert(
3024                        "play".into(),
3025                        Value::Object(Obj::native("play", |_, _, _| Ok(Value::Undefined))),
3026                    );
3027                    b.props.insert(
3028                        "pause".into(),
3029                        Value::Object(Obj::native("pause", |_, _, _| Ok(Value::Undefined))),
3030                    );
3031                    b.props.insert(
3032                        "cancel".into(),
3033                        Value::Object(Obj::native("cancel", |_, _, _| Ok(Value::Undefined))),
3034                    );
3035                    // `reverse()`/`finish()`(丸ごと未対応だった。`play`/
3036                    // `pause`/`cancel`と同じ「実際のアニメーション実行機構が
3037                    // 無いno-op」の簡略方針。2026-07-17 発見・実装)。
3038                    b.props.insert(
3039                        "reverse".into(),
3040                        Value::Object(Obj::native("reverse", |_, _, _| Ok(Value::Undefined))),
3041                    );
3042                    b.props.insert(
3043                        "finish".into(),
3044                        Value::Object(Obj::native("finish", |_, _, _| Ok(Value::Undefined))),
3045                    );
3046                    b.props.insert(
3047                        "finished".into(),
3048                        Value::Object(super::super::builtins::make_resolved_promise(Value::Undefined)),
3049                    );
3050                    b.props.insert("playState".into(), Value::str("finished"));
3051                    b.props.insert("currentTime".into(), Value::Number(0.0));
3052                    // `playbackRate`(丸ごと未対応だった。読み書き可能な
3053                    // 単純プロパティとして反映するのみで、実際の再生速度への
3054                    // 影響は対象外)。
3055                    b.props.insert("playbackRate".into(), Value::Number(1.0));
3056                }
3057                Ok(Value::Object(anim))
3058            })),
3059            // --- HTML5 制約バリデーション ---
3060            "checkValidity" => Value::Object(Obj::native(
3061                "checkValidity",
3062                super::super::builtins::dom_check_validity,
3063            )),
3064            "reportValidity" => Value::Object(Obj::native(
3065                "reportValidity",
3066                super::super::builtins::dom_check_validity,
3067            )),
3068            "setCustomValidity" => Value::Object(Obj::native(
3069                "setCustomValidity",
3070                super::super::builtins::dom_set_custom_validity,
3071            )),
3072            "validationMessage" => {
3073                let v = self.dom.borrow().effective_form_value(idx);
3074                Value::str(
3075                    self.dom
3076                        .borrow()
3077                        .validate_field(idx, &v)
3078                        .unwrap_or_default(),
3079                )
3080            }
3081            "willValidate" => {
3082                // 仕様上「制約検証の対象候補」になれるのは`button`/`input`/
3083                // `select`/`textarea`の4タグのみ(`output`は明示的に対象外、
3084                // `fieldset`/`object`等の非フォームコントロールも対象外)。
3085                // 以前はタグを一切見ておらず、`type`/`disabled`だけで判定
3086                // していたため、`<output>`や`<div>`のような本来常に`false`
3087                // であるべき要素まで`type`属性が無い(`ty==""`)ことで
3088                // 除外リストに引っかからず`true`になってしまう実害のある
3089                // バグだった。2026-07-18 発見・修正)。
3090                let dom = self.dom.borrow();
3091                let tag = dom.nodes.get(idx).map(|n| n.tag.as_str()).unwrap_or("");
3092                let is_candidate_tag = matches!(tag, "button" | "input" | "select" | "textarea");
3093                let disabled = dom.has_attr(idx, "disabled");
3094                let ty = dom.get_attr(idx, "type").unwrap_or_default().to_lowercase();
3095                let excluded_type = matches!(
3096                    ty.as_str(),
3097                    "submit" | "button" | "reset" | "hidden" | "image"
3098                );
3099                Value::Bool(is_candidate_tag && !disabled && !excluded_type)
3100            }
3101            // `element.validity` が `valid`/`valueMissing`/`customError` の3つしか
3102            // 公開しておらず、`.validity.patternMismatch`/`.typeMismatch`/
3103            // `.rangeOverflow`/`.rangeUnderflow`/`.stepMismatch`/`.tooLong`/
3104            // `.tooShort`/`.badInput` という特定の制約を狙い撃ちしてエラーメッセージを
3105            // 出し分ける ValidityState の標準的な使い方(`patternMismatch` だけ拾って
3106            // 独自メッセージを出す等)が常に `undefined`(falsy)になっていたバグ。
3107            "validity" => {
3108                let v = self.dom.borrow().effective_form_value(idx);
3109                let flags = self.dom.borrow().validity_flags(idx, &v);
3110                let vs = Obj::plain();
3111                {
3112                    let mut b = vs.borrow_mut();
3113                    b.props.insert("valid".into(), Value::Bool(flags.valid()));
3114                    b.props
3115                        .insert("valueMissing".into(), Value::Bool(flags.value_missing));
3116                    b.props
3117                        .insert("typeMismatch".into(), Value::Bool(flags.type_mismatch));
3118                    b.props
3119                        .insert("patternMismatch".into(), Value::Bool(flags.pattern_mismatch));
3120                    b.props.insert("tooLong".into(), Value::Bool(flags.too_long));
3121                    b.props.insert("tooShort".into(), Value::Bool(flags.too_short));
3122                    b.props
3123                        .insert("rangeOverflow".into(), Value::Bool(flags.range_overflow));
3124                    b.props
3125                        .insert("rangeUnderflow".into(), Value::Bool(flags.range_underflow));
3126                    b.props
3127                        .insert("stepMismatch".into(), Value::Bool(flags.step_mismatch));
3128                    b.props.insert("badInput".into(), Value::Bool(flags.bad_input));
3129                    b.props
3130                        .insert("customError".into(), Value::Bool(flags.custom_error));
3131                }
3132                Value::Object(vs)
3133            }
3134            // ARIA 反映 IDL 属性(`element.role`/`.ariaLabel`/`.ariaChecked` 等)が
3135            // 丸ごと未対応だった。`aria-*` 属性は複合語内部にハイフンを持たない
3136            // (例: `aria-valuenow`。`aria-value-now` ではない)ため、`dataset` と
3137            // 同じ単純な camelCase⇄kebab-case変換は使えず、`aria_attr_name` の
3138            // 固定対応表で個別に引く。未設定なら(`dataset` の空文字列とは異なり)
3139            // 仕様どおり `null`。
3140            key if aria_attr_name(key).is_some() => {
3141                let attr = aria_attr_name(key).unwrap_or_default();
3142                match self.dom.borrow().get_attr(idx, attr) {
3143                    Some(v) => Value::str(v),
3144                    None => Value::Null,
3145                }
3146            }
3147            // GlobalEventHandlers(element.onchange/element.onclick 等)。
3148            // 【2026-07-23】関数値は`Obj::dom(idx)`(呼び出す度に使い捨てで新規生成
3149            // される)の`.props`ではなく、永続ストレージ`DomBridge::on_handlers`
3150            // から読む(詳細は`dom_bridge.rs`の`on_handlers`フィールドコメント参照)。
3151            key if key.starts_with("on") && key.len() > 2 => {
3152                if let Some(val) = self.dom.borrow().get_on_handler(idx, key) {
3153                    if !matches!(val, Value::Undefined) {
3154                        return val;
3155                    }
3156                }
3157                match self.dom.borrow().get_attr(idx, key) {
3158                    Some(v) => Value::str(v),
3159                    None => Value::Null,
3160                }
3161            }
3162            // 組込みに無い名前は原型の連鎖(HTMLXxxElement.prototype → … → Node.prototype →
3163            // EventTarget.prototype)から引く。アクセサは要素を this にして呼ぶ。
3164            // 【2026-10-19】ShadyDOM(YouTube の視聴ページ)が原型に定義する `__shady_native_*` 等が
3165            // 要素から見えず「is not a function」で止まっていた。
3166            _ if builtin_only => Value::Undefined,
3167            _ => self.dom_proto_chain_get(idx, key),
3168        }
3169    }
3170
3171    /// DOM 要素の原型の連鎖から `key` を引く(無ければ undefined)。
3172    fn dom_proto_chain_get(&mut self, idx: usize, key: &str) -> Value {
3173        let recv = Value::Object(Obj::dom(idx));
3174        let mut cur = self.get_dom_node_proto(idx);
3175        let mut guard = 0;
3176        while let Some(p) = cur {
3177            let getter = p.borrow().accessors.get(key).map(|a| a.get.clone());
3178            if let Some(g) = getter {
3179                return match g {
3180                    Some(g) => self.call_value(&g, recv, &[]).unwrap_or(Value::Undefined),
3181                    None => Value::Undefined,
3182                };
3183            }
3184            if let Some(v) = p.borrow().props.get(key).cloned() {
3185                return v;
3186            }
3187            guard += 1;
3188            if guard > 64 {
3189                break;
3190            }
3191            cur = p.borrow().proto.clone();
3192        }
3193        // 連鎖の終端は Object.prototype(組込みのメソッドは実体から解決する方式なので、ここで補う)。
3194        // 【2026-10-20】Polymer の iron-iconset-svg が `this.hasOwnProperty(…)` で落ちていた。
3195        super::super::builtins::object_get(&Obj::plain(), key)
3196    }
3197
3198    pub(crate) fn dom_set_property(&mut self, disp: DomDisp, key: &str, val: Value) {
3199        let s = val.to_js_string();
3200        // `textContent`/`innerHTML`/`innerText`/`nodeValue` は WebIDL 上 nullable な
3201        // `DOMString?`(または `[LegacyNullToEmptyString]`)として定義されており、
3202        // `null`/`undefined` の代入は通常の `ToString`(`"null"`/`"undefined"` という
3203        // 文字列)ではなく空文字列に変換されるべき。以前は他のプロパティ(`id`/
3204        // `className` 等、nullable でない通常の `DOMString`)と同じ `to_js_string()`
3205        // を無条件で使っており、`el.textContent = null` が空にする代わりに文字通り
3206        // "null" というテキストを設定してしまう静かなバグだった。
3207        let s_nullable = if matches!(val, Value::Null | Value::Undefined) {
3208            String::new()
3209        } else {
3210            s.clone()
3211        };
3212        match disp {
3213            DomDisp::Element(idx) => match key {
3214                "textContent" | "innerText" | "text" => {
3215                    self.dom.borrow_mut().set_text_content(idx, &s_nullable)
3216                }
3217                // `element.outerText`(丸ごと未対応だった。読み出しは
3218                // `innerText`と同じだが、書き込みは`innerText`と違って
3219                // 自身の中身ではなく自身「そのもの」をテキストノードへ
3220                // 置き換える点が違う。親が無ければ仕様上例外だが、他の
3221                // 簡略実装と同じ方針でno-opにする)。2026-07-18
3222                // 発見・実装)。
3223                "outerText" => {
3224                    let parent = self.dom.borrow().nodes.get(idx).and_then(|n| n.parent);
3225                    if let Some(parent) = parent {
3226                        let text_idx = self.dom.borrow_mut().create_text_node(&s_nullable);
3227                        self.dom.borrow_mut().replace_child(parent, text_idx, idx);
3228                    }
3229                }
3230                // `nodeValue` の代入は仕様上テキストノードのみ効果を持つ(要素ノードへの
3231                // 代入は no-op)。`textContent` と違い要素の子を丸ごと差し替えるわけでは
3232                // ないため、`set_text_content` をそのまま流用してはいけない
3233                // (テキストノード自身は子を持たないため結果的に同じ関数で正しく効くが、
3234                // 要素ノードでは意図的に何もしない点が異なる)。
3235                "nodeValue" => {
3236                    let has_val = self.dom.borrow().nodes.get(idx).map(|n| n.is_text || n.tag == "#comment").unwrap_or(false);
3237                    if has_val {
3238                        self.dom.borrow_mut().set_text_content(idx, &s_nullable);
3239                    }
3240                }
3241                // `object.data`(読み出し側と対になる書き込み。
3242                // `CharacterData.data`と衝突するためタグ限定で先に判定する。
3243                // 2026-07-17 発見・実装)。
3244                "data" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "object").unwrap_or(false) => {
3245                    self.dom.borrow_mut().set_attr(idx, "data", &s);
3246                }
3247                // `CharacterData.data`(`nodeValue`と対になる書き込み側。
3248                // 2026-07-16 発見・実装)。
3249                "data" => {
3250                    let has_data = self.dom.borrow().nodes.get(idx).map(|n| n.is_text || n.tag == "#comment").unwrap_or(false);
3251                    if has_data {
3252                        self.dom.borrow_mut().set_text_content(idx, &s_nullable);
3253                    }
3254                }
3255                "innerHTML" => {
3256                    let is_template = self.dom.borrow().nodes.get(idx).map(|n| n.tag == "template").unwrap_or(false);
3257                    if is_template {
3258                        let frag_val = self.dom_element_get(idx, "content");
3259                        if let Some(fi) = this_dom_idx(&frag_val) {
3260                            self.dom.borrow_mut().set_inner_html(fi, &s_nullable);
3261                        } else {
3262                            self.dom.borrow_mut().set_inner_html(idx, &s_nullable);
3263                        }
3264                    } else {
3265                        self.dom.borrow_mut().set_inner_html(idx, &s_nullable);
3266                    }
3267                }
3268                "outerHTML" => self.dom.borrow_mut().set_outer_html(idx, &s_nullable),
3269                "id" => self.dom.borrow_mut().set_attr(idx, "id", &s),
3270                "className" => self.dom.borrow_mut().set_attr(idx, "class", &s),
3271                "value" => {
3272                    let tag = self.dom.borrow().nodes.get(idx).map(|n| n.tag.clone()).unwrap_or_default();
3273                    if tag == "select" {
3274                        self.dom.borrow_mut().set_select_value(idx, &s);
3275                    } else if tag == "input" || tag == "textarea" {
3276                        let sanitized = if tag == "input"
3277                            && self.dom.borrow().get_attr(idx, "type").as_deref() == Some("color")
3278                        {
3279                            let is_valid_hex = s.len() == 7
3280                                && s.starts_with('#')
3281                                && s.chars().skip(1).all(|c| c.is_ascii_hexdigit());
3282                            if is_valid_hex {
3283                                s.to_lowercase()
3284                            } else {
3285                                String::from("#000000")
3286                            }
3287                        } else {
3288                            s
3289                        };
3290                        self.dom.borrow_mut().set_attr(idx, "_live_value", &sanitized);
3291                    } else {
3292                        self.dom.borrow_mut().set_attr(idx, "value", &s);
3293                    }
3294                }
3295                "valueAsNumber" => {
3296                    let n = val.to_number();
3297                    let mut dom = self.dom.borrow_mut();
3298                    if n.is_nan() {
3299                        dom.set_attr(idx, "_live_value", "");
3300                        dom.set_attr(idx, "value", "");
3301                    } else {
3302                        let str_v = Value::Number(n).to_js_string();
3303                        dom.set_attr(idx, "_live_value", &str_v);
3304                        dom.set_attr(idx, "value", &str_v);
3305                    }
3306                }
3307                "valueAsDate" => {
3308                    let ty = self.dom.borrow().get_attr(idx, "type").unwrap_or_default();
3309                    let mut dom = self.dom.borrow_mut();
3310                    if matches!(val, Value::Null | Value::Undefined) {
3311                        dom.set_attr(idx, "_live_value", "");
3312                        dom.set_attr(idx, "value", "");
3313                    } else {
3314                        let ms = super::super::builtins::date_value_ms(&val);
3315                        if let Some(s) = super::super::builtins::input_date_ms_to_value(&ty, ms) {
3316                            dom.set_attr(idx, "_live_value", &s);
3317                            dom.set_attr(idx, "value", &s);
3318                        } else {
3319                            dom.set_attr(idx, "_live_value", "");
3320                            dom.set_attr(idx, "value", "");
3321                        }
3322                    }
3323                }
3324                "selectedIndex" => {
3325                    let n = val.to_number() as i32;
3326                    self.dom.borrow_mut().set_select_selected_index(idx, n);
3327                }
3328                // `select.length = n`(`select.options.length`と対になる書き込み側。
3329                // 仕様どおり負数・NaNは`0`扱い。丸ごと未対応だった)。`<select>`
3330                // 以外への誤代入で無関係な`<option>`子孫が操作されないよう
3331                // タグ判定でガードする。
3332                "length"
3333                    if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "select").unwrap_or(false) =>
3334                {
3335                    let n = val.to_number();
3336                    let n = if n.is_finite() && n > 0.0 { n as usize } else { 0 };
3337                    self.dom.borrow_mut().set_select_length(idx, n);
3338                }
3339                // `selectionStart`/`selectionEnd`/`selectionDirection`
3340                // (読み出し側と対になる書き込み。内部専用属性へそのまま
3341                // 保存するだけの状態追跡のみの簡略実装)。
3342                "selectionStart" => self.dom.borrow_mut().set_attr(idx, "_selection_start", &s),
3343                "selectionEnd" => self.dom.borrow_mut().set_attr(idx, "_selection_end", &s),
3344                "selectionDirection" => self.dom.borrow_mut().set_attr(idx, "_selection_direction", &s),
3345                "type" => self.dom.borrow_mut().set_attr(idx, "type", &s),
3346                // `input`/`textarea.defaultValue`/`input.defaultChecked`/`option.defaultSelected`
3347                // (読み出し側と対になる書き込み。2026-07-17 発見・実装)。
3348                "defaultValue" => self.dom.borrow_mut().set_attr(idx, "value", &s),
3349                "defaultChecked" => {
3350                    let mut dom = self.dom.borrow_mut();
3351                    if val.truthy() {
3352                        dom.set_attr(idx, "checked", "checked");
3353                    } else {
3354                        dom.remove_attr(idx, "checked");
3355                    }
3356                }
3357                "defaultSelected" => {
3358                    let mut dom = self.dom.borrow_mut();
3359                    if val.truthy() {
3360                        dom.set_attr(idx, "selected", "selected");
3361                    } else {
3362                        dom.remove_attr(idx, "selected");
3363                    }
3364                }
3365                // `checkbox.checked = true` に加え、`radio.checked = true` も
3366                // `element.click()` と同じくラジオボタングループの排他選択
3367                // (同じ `name` を持つ他の radio の `checked` を外す)を効かせる。
3368                "checked" => {
3369                    let mut dom = self.dom.borrow_mut();
3370                    if val.truthy() {
3371                        dom.set_attr(idx, "_live_checked", "true");
3372                        dom.uncheck_radio_group_siblings(idx);
3373                    } else {
3374                        dom.set_attr(idx, "_live_checked", "false");
3375                    }
3376                }
3377                // `dialog.open = true/false`(`show()`/`close()` と同じ効果の直接プロパティ
3378                // 代入)。
3379                "open" => {
3380                    let mut dom = self.dom.borrow_mut();
3381                    if val.truthy() {
3382                        dom.set_attr(idx, "open", "open");
3383                    } else {
3384                        dom.remove_attr(idx, "open");
3385                    }
3386                }
3387                // `element.inert = true/false`(属性の付け外し)。
3388                "inert" => {
3389                    let mut dom = self.dom.borrow_mut();
3390                    if val.truthy() {
3391                        dom.set_attr(idx, "inert", "");
3392                    } else {
3393                        dom.remove_attr(idx, "inert");
3394                    }
3395                }
3396                // `dialog.returnValue = 'x'`(`close()` を介さない直接代入)。
3397                "returnValue" => {
3398                    self.dom
3399                        .borrow_mut()
3400                        .set_attr(idx, "_return_value", &val.to_js_string());
3401                }
3402                "indeterminate" => {
3403                    let mut dom = self.dom.borrow_mut();
3404                    if val.truthy() {
3405                        dom.set_attr(idx, "_indeterminate", "true");
3406                    } else {
3407                        dom.remove_attr(idx, "_indeterminate");
3408                    }
3409                }
3410                "disabled" => {
3411                    let mut dom = self.dom.borrow_mut();
3412                    if val.truthy() {
3413                        dom.set_attr(idx, "disabled", "disabled");
3414                    } else {
3415                        dom.remove_attr(idx, "disabled");
3416                    }
3417                }
3418                "selected" => {
3419                    let mut dom = self.dom.borrow_mut();
3420                    if val.truthy() {
3421                        dom.set_attr(idx, "selected", "selected");
3422                    } else {
3423                        dom.remove_attr(idx, "selected");
3424                    }
3425                }
3426
3427                "placeholder" => self.dom.borrow_mut().set_attr(idx, "placeholder", &s),
3428                // `input.pattern`(読み出し側と対になる書き込み。
3429                // 2026-07-17 発見・実装)。
3430                "pattern" => self.dom.borrow_mut().set_attr(idx, "pattern", &s),
3431                // `input.capture`(読み出し側と対になる書き込み。
3432                // 2026-07-17 発見・実装)。
3433                "capture" => self.dom.borrow_mut().set_attr(idx, "capture", &s),
3434                // `input.accept`(読み出し側と対になる書き込み。
3435                // 2026-07-18 発見・実装)。
3436                "accept" => self.dom.borrow_mut().set_attr(idx, "accept", &s),
3437                "name" => self.dom.borrow_mut().set_attr(idx, "name", &s),
3438                "required" => {
3439                    let mut dom = self.dom.borrow_mut();
3440                    if val.truthy() {
3441                        dom.set_attr(idx, "required", "required");
3442                    } else {
3443                        dom.remove_attr(idx, "required");
3444                    }
3445                }
3446                "noValidate" => {
3447                    let mut dom = self.dom.borrow_mut();
3448                    if val.truthy() {
3449                        dom.set_attr(idx, "novalidate", "novalidate");
3450                    } else {
3451                        dom.remove_attr(idx, "novalidate");
3452                    }
3453                }
3454                "formNoValidate" => {
3455                    let mut dom = self.dom.borrow_mut();
3456                    if val.truthy() {
3457                        dom.set_attr(idx, "formnovalidate", "formnovalidate");
3458                    } else {
3459                        dom.remove_attr(idx, "formnovalidate");
3460                    }
3461                }
3462                // `ol.reversed`/`.start`(読み出し側と対になる書き込み。
3463                // 2026-07-17 発見・実装)。
3464                "reversed" => {
3465                    let mut dom = self.dom.borrow_mut();
3466                    if val.truthy() {
3467                        dom.set_attr(idx, "reversed", "reversed");
3468                    } else {
3469                        dom.remove_attr(idx, "reversed");
3470                    }
3471                }
3472                "start" => self.dom.borrow_mut().set_attr(idx, "start", &s),
3473                "readOnly" => {
3474                    let mut dom = self.dom.borrow_mut();
3475                    if val.truthy() {
3476                        dom.set_attr(idx, "readonly", "readonly");
3477                    } else {
3478                        dom.remove_attr(idx, "readonly");
3479                    }
3480                }
3481                // `element.hidden = true/false`(要素の表示/非表示を切り替える定番
3482                // イディオム。`classList.toggle` と並んでよく使われる)が丸ごと未対応
3483                // だった。getter(`hidden` 属性の有無を返す)は既に対応済みだったが、
3484                // 対をなす setter が漏れていた。
3485                "hidden" => {
3486                    let mut dom = self.dom.borrow_mut();
3487                    if val.truthy() {
3488                        dom.set_attr(idx, "hidden", "hidden");
3489                    } else {
3490                        dom.remove_attr(idx, "hidden");
3491                    }
3492                }
3493                "multiple" => {
3494                    let mut dom = self.dom.borrow_mut();
3495                    if val.truthy() {
3496                        dom.set_attr(idx, "multiple", "multiple");
3497                    } else {
3498                        dom.remove_attr(idx, "multiple");
3499                    }
3500                }
3501                "src" => {
3502                    let s_val = s.clone();
3503                    self.dom.borrow_mut().set_attr(idx, "src", &s);
3504                    let is_media = self
3505                        .dom
3506                        .borrow()
3507                        .nodes
3508                        .get(idx)
3509                        .map(|n| n.tag == "video" || n.tag == "audio")
3510                        .unwrap_or(false);
3511                    if is_media && s_val.starts_with("blob:") {
3512                        let ms_val = match self.global.borrow().vars.get("__media_source_urls") {
3513                            Some(Value::Object(r)) => r.borrow().props.get(&s_val).cloned(),
3514                            _ => None,
3515                        };
3516                        if let Some(ms) = ms_val {
3517                            super::super::builtins::media_source_open_internal(self, &ms);
3518                        }
3519                    }
3520                }
3521                "srcObject" => {
3522                    let is_media = self
3523                        .dom
3524                        .borrow()
3525                        .nodes
3526                        .get(idx)
3527                        .map(|n| n.tag == "video" || n.tag == "audio")
3528                        .unwrap_or(false);
3529                    if is_media {
3530                        let is_ms = if let Value::Object(o) = &val {
3531                            let b = o.borrow();
3532                            b.props.contains_key("sourceBuffers")
3533                                || b.props.get("_is_media_source").map(|v| v.truthy()).unwrap_or(false)
3534                        } else {
3535                            false
3536                        };
3537                        if is_ms {
3538                            super::super::builtins::media_source_open_internal(self, &val);
3539                        }
3540                    }
3541                }
3542                "alt" => self.dom.borrow_mut().set_attr(idx, "alt", &s),
3543                "autoplay" => {
3544                    let mut dom = self.dom.borrow_mut();
3545                    if val.truthy() {
3546                        dom.set_attr(idx, "autoplay", "autoplay");
3547                    } else {
3548                        dom.remove_attr(idx, "autoplay");
3549                    }
3550                }
3551                "controls" => {
3552                    let mut dom = self.dom.borrow_mut();
3553                    if val.truthy() {
3554                        dom.set_attr(idx, "controls", "controls");
3555                    } else {
3556                        dom.remove_attr(idx, "controls");
3557                    }
3558                }
3559                "loop" => {
3560                    let mut dom = self.dom.borrow_mut();
3561                    if val.truthy() {
3562                        dom.set_attr(idx, "loop", "loop");
3563                    } else {
3564                        dom.remove_attr(idx, "loop");
3565                    }
3566                }
3567                "muted" => {
3568                    let is_muted = val.truthy();
3569                    {
3570                        let mut dom = self.dom.borrow_mut();
3571                        if is_muted {
3572                            dom.set_attr(idx, "muted", "muted");
3573                        } else {
3574                            dom.remove_attr(idx, "muted");
3575                        }
3576                    }
3577                    // 【2026-10-19】再生状態(全体で 1 つ)を変えるのは、再生中(またはこれから再生する)
3578                    // 要素だけ。YouTube は消音の設定の前に別の試験用 <video> の muted を反転させて
3579                    // 設定できるかを調べる(`Dd()`)ため、それで本物の再生まで消音されていた。
3580                    let src = self.dom.borrow().get_attr(idx, "src").unwrap_or_default();
3581                    let mut pb = crate::os_lib::mse::PLAYBACK.lock();
3582                    if !src.is_empty() && (pb.url.is_empty() || pb.url == src) {
3583                        pb.muted = is_muted;
3584                        drop(pb);
3585                        if is_muted {
3586                            crate::kernel::audio::Audio::mute();
3587                        }
3588                    }
3589                    // `.volume` と同じく仕様どおり `volumechange` を発火する。
3590                    let _ = self.dispatch_event_in_interp(idx, "volumechange", &[]);
3591                }
3592                "playsInline" => {
3593                    let mut dom = self.dom.borrow_mut();
3594                    if val.truthy() {
3595                        dom.set_attr(idx, "playsinline", "playsinline");
3596                    } else {
3597                        dom.remove_attr(idx, "playsinline");
3598                    }
3599                }
3600                // `video.disablePictureInPicture`/`iframe.allowFullscreen`
3601                // (読み出し側と対になる書き込み。2026-07-17 発見・実装)。
3602                "disablePictureInPicture" => {
3603                    let mut dom = self.dom.borrow_mut();
3604                    if val.truthy() {
3605                        dom.set_attr(idx, "disablepictureinpicture", "disablepictureinpicture");
3606                    } else {
3607                        dom.remove_attr(idx, "disablepictureinpicture");
3608                    }
3609                }
3610                // `video.disableRemotePlayback`(読み出し側と対になる書き込み。
3611                // 2026-07-18 発見・実装)。
3612                "disableRemotePlayback" => {
3613                    let mut dom = self.dom.borrow_mut();
3614                    if val.truthy() {
3615                        dom.set_attr(idx, "disableremoteplayback", "disableremoteplayback");
3616                    } else {
3617                        dom.remove_attr(idx, "disableremoteplayback");
3618                    }
3619                }
3620                "allowFullscreen" => {
3621                    let mut dom = self.dom.borrow_mut();
3622                    if val.truthy() {
3623                        dom.set_attr(idx, "allowfullscreen", "allowfullscreen");
3624                    } else {
3625                        dom.remove_attr(idx, "allowfullscreen");
3626                    }
3627                }
3628                // `iframe.credentialless`(読み出し側と対になる書き込み。
3629                // 2026-07-18 発見・実装)。
3630                "credentialless" => {
3631                    let mut dom = self.dom.borrow_mut();
3632                    if val.truthy() {
3633                        dom.set_attr(idx, "credentialless", "credentialless");
3634                    } else {
3635                        dom.remove_attr(idx, "credentialless");
3636                    }
3637                }
3638                // `canvas`/`img`/`video.width`/`.height`(読み出し側と対になる
3639                // 書き込み。2026-07-17 発見・実装)。
3640                "width"
3641                    if self
3642                        .dom
3643                        .borrow()
3644                        .nodes
3645                        .get(idx)
3646                        .map(|n| n.tag == "canvas" || n.tag == "img" || n.tag == "video")
3647                        .unwrap_or(false) =>
3648                {
3649                    self.dom.borrow_mut().set_attr(idx, "width", &s);
3650                }
3651                "height"
3652                    if self
3653                        .dom
3654                        .borrow()
3655                        .nodes
3656                        .get(idx)
3657                        .map(|n| n.tag == "canvas" || n.tag == "img" || n.tag == "video")
3658                        .unwrap_or(false) =>
3659                {
3660                    self.dom.borrow_mut().set_attr(idx, "height", &s);
3661                }
3662                // `embed`/`object`/`iframe.width`/`.height`(読み出し側と対に
3663                // なる書き込み。単純な文字列反映。2026-07-17 発見・実装)。
3664                "width"
3665                    if self
3666                        .dom
3667                        .borrow()
3668                        .nodes
3669                        .get(idx)
3670                        .map(|n| n.tag == "embed" || n.tag == "object" || n.tag == "iframe")
3671                        .unwrap_or(false) =>
3672                {
3673                    self.dom.borrow_mut().set_attr(idx, "width", &s);
3674                }
3675                "height"
3676                    if self
3677                        .dom
3678                        .borrow()
3679                        .nodes
3680                        .get(idx)
3681                        .map(|n| n.tag == "embed" || n.tag == "object" || n.tag == "iframe")
3682                        .unwrap_or(false) =>
3683                {
3684                    self.dom.borrow_mut().set_attr(idx, "height", &s);
3685                }
3686                // `.currentTime`/`.volume`/`.playbackRate` の直接代入(`.play()`/
3687                // `.pause()`と同じ内部専用属性への状態保存のみの簡略実装)。
3688                // `.volume` の範囲外値(0〜1 外)は仕様上 `IndexSizeError` だが、
3689                // 他の簡略実装同様エラーは投げず `clamp` する。
3690                // 仕様どおり `.currentTime`/`.volume`/`.playbackRate` の直接代入は
3691                // それぞれ `seeked`/`volumechange`/`ratechange` イベントを発火する
3692                // (`.play()`/`.pause()`の `play`/`pause` イベント発火と対)。
3693                "currentTime" => {
3694                    let t = val.to_number();
3695                    self.dom.borrow_mut().set_attr(idx, "_current_time", &t.to_string());
3696                    // 実際にシークする(再生スレッドが取り出す。再生開始前の代入は開始時に効く)。
3697                    let has_src = self.dom.borrow().get_attr(idx, "src").is_some_and(|s| !s.is_empty());
3698                    if t.is_finite() && has_src {
3699                        *crate::os_lib::mse::SEEK_REQUEST.lock() = Some(t.max(0.0));
3700                        crate::os_lib::mse::PLAYBACK.lock().current_time = t.max(0.0);
3701                    }
3702                    let _ = self.dispatch_event_in_interp(idx, "seeking", &[]);
3703                    let _ = self.dispatch_event_in_interp(idx, "seeked", &[]);
3704                }
3705                "volume" => {
3706                    let v = val.to_number().clamp(0.0, 1.0);
3707                    self.dom.borrow_mut().set_attr(idx, "_volume", &v.to_string());
3708                    let _ = crate::os_lib::mse::PLAYBACK.lock().set_volume(v);
3709                    if v <= 0.0 {
3710                        crate::kernel::audio::Audio::mute();
3711                    }
3712                    let _ = self.dispatch_event_in_interp(idx, "volumechange", &[]);
3713                }
3714                "playbackRate" => {
3715                    let r = val.to_number();
3716                    self.dom.borrow_mut().set_attr(idx, "_playback_rate", &r.to_string());
3717                    let _ = crate::os_lib::mse::PLAYBACK.lock().set_playback_rate(r);
3718                    let _ = self.dispatch_event_in_interp(idx, "ratechange", &[]);
3719                }
3720                "defaultPlaybackRate" => self
3721                    .dom
3722                    .borrow_mut()
3723                    .set_attr(idx, "_default_playback_rate", &val.to_number().to_string()),
3724                "nonce" => self.dom.borrow_mut().set_attr(idx, "nonce", &s),
3725                // `element.role`/`.ariaLabel`等(読み出し側と対になる書き込み。
3726                // `null`代入は仕様上属性除去を意味するため`remove_attr`する。
3727                // 2026-07-18 発見・実装)。
3728                "role" => {
3729                    if matches!(val, Value::Null) {
3730                        self.dom.borrow_mut().remove_attr(idx, "role");
3731                    } else {
3732                        self.dom.borrow_mut().set_attr(idx, "role", &s);
3733                    }
3734                }
3735                // `ariaActiveDescendantElement`(読み出し側と対になる書き込み。
3736                // `popoverTargetElement`と同じidref解決パターン。汎用の
3737                // `aria-*`文字列反映アームより前に置く必要がある
3738                // (`starts_with("aria")`の汎用ガードに先に吸われてしまう
3739                // ため)。2026-07-18 発見・実装)。
3740                // `aria*Elements`(読み出し側と対になる書き込み。配列/
3741                // iterable な要素参照の並びをidref文字列へ空白区切りで
3742                // 直列化する。`null`/`undefined`代入は属性除去。要素が
3743                // `id`を持たない場合はそのエントリのみ無視する。
3744                // `ariaActiveDescendantElement`と同じ理由で汎用の
3745                // `aria-*`文字列反映アームより前に置く必要がある。
3746                // 2026-07-18 発見・実装)。
3747                _ if key.starts_with("aria") && key.ends_with("Elements") && key.len() > 12 => {
3748                    let suffix: String =
3749                        key.chars().skip(4).take(key.len() - 4 - 8).collect::<String>().to_lowercase();
3750                    let attr = alloc::format!("aria-{}", suffix);
3751                    if matches!(val, Value::Null | Value::Undefined) {
3752                        self.dom.borrow_mut().remove_attr(idx, &attr);
3753                    } else {
3754                        let items = self.iterate_values(&val, true);
3755                        let ids: Vec<String> = items
3756                            .iter()
3757                            .filter_map(|v| match v {
3758                                Value::Object(o) => match &o.borrow().kind {
3759                                    ObjKind::DomElement(ti) => {
3760                                        self.dom.borrow().nodes.get(*ti).map(|n| n.id.clone())
3761                                    }
3762                                    _ => None,
3763                                },
3764                                _ => None,
3765                            })
3766                            .filter(|s| !s.is_empty())
3767                            .collect();
3768                        self.dom.borrow_mut().set_attr(idx, &attr, &ids.join(" "));
3769                    }
3770                }
3771                "ariaActiveDescendantElement" => match val {
3772                    Value::Null => {
3773                        self.dom.borrow_mut().remove_attr(idx, "aria-activedescendant");
3774                    }
3775                    Value::Object(o) if matches!(&o.borrow().kind, ObjKind::DomElement(_)) => {
3776                        if let ObjKind::DomElement(ti) = &o.borrow().kind {
3777                            let target_id = self.dom.borrow().nodes.get(*ti).map(|n| n.id.clone());
3778                            if let Some(tid) = target_id.filter(|s| !s.is_empty()) {
3779                                self.dom.borrow_mut().set_attr(idx, "aria-activedescendant", &tid);
3780                            }
3781                        }
3782                    }
3783                    _ => {}
3784                },
3785                _ if key.starts_with("aria") && key.as_bytes().get(4).is_some_and(u8::is_ascii_uppercase) => {
3786                    let suffix: String = key.chars().skip(4).collect::<String>().to_lowercase();
3787                    let attr = alloc::format!("aria-{}", suffix);
3788                    if matches!(val, Value::Null) {
3789                        self.dom.borrow_mut().remove_attr(idx, &attr);
3790                    } else {
3791                        self.dom.borrow_mut().set_attr(idx, &attr, &s);
3792                    }
3793                }
3794                "preload" => self.dom.borrow_mut().set_attr(idx, "preload", &s),
3795                // `textarea.wrap`(読み出し側と対になる書き込み。
3796                // 2026-07-17 発見・実装)。
3797                "wrap" => self.dom.borrow_mut().set_attr(idx, "wrap", &s),
3798                "poster" => self.dom.borrow_mut().set_attr(idx, "poster", &s),
3799                // `track.kind`(読み出し側と対になる書き込み。2026-07-17 発見・実装)。
3800                "kind" => self.dom.borrow_mut().set_attr(idx, "kind", &s),
3801                // `option`/`optgroup`/`track.label`(読み出し側は個別に
3802                // タグ限定ゲッターがあるが、書き込み側の`label`セッター
3803                // 自体がどのタグに対しても丸ごと存在しなかった。単純な
3804                // 属性反映のため3タグ共通の1本にまとめる。`track.srclang`/
3805                // `.default`とあわせて2026-07-18 発見・実装)。
3806                "label" => self.dom.borrow_mut().set_attr(idx, "label", &s),
3807                "srclang" => self.dom.borrow_mut().set_attr(idx, "srclang", &s),
3808                "default" => {
3809                    let mut dom = self.dom.borrow_mut();
3810                    if val.truthy() {
3811                        dom.set_attr(idx, "default", "default");
3812                    } else {
3813                        dom.remove_attr(idx, "default");
3814                    }
3815                }
3816                "ping" => self.dom.borrow_mut().set_attr(idx, "ping", &s),
3817                "hreflang" => self.dom.borrow_mut().set_attr(idx, "hreflang", &s),
3818                "srcset" => self.dom.borrow_mut().set_attr(idx, "srcset", &s),
3819                "sizes" => self.dom.borrow_mut().set_attr(idx, "sizes", &s),
3820                "media" => self.dom.borrow_mut().set_attr(idx, "media", &s),
3821                "useMap" => self.dom.borrow_mut().set_attr(idx, "usemap", &s),
3822                "isMap" => {
3823                    let mut dom = self.dom.borrow_mut();
3824                    if val.truthy() {
3825                        dom.set_attr(idx, "ismap", "ismap");
3826                    } else {
3827                        dom.remove_attr(idx, "ismap");
3828                    }
3829                }
3830                "referrerPolicy" => self.dom.borrow_mut().set_attr(idx, "referrerpolicy", &s),
3831                // `link.as`(読み出し側と対になる書き込み。2026-07-18 発見・実装)。
3832                "as" => self.dom.borrow_mut().set_attr(idx, "as", &s),
3833                // `link.imageSrcset`/`.imageSizes`(読み出し側と対になる書き込み。
3834                // 2026-07-18 発見・実装)。
3835                "imageSrcset" => self.dom.borrow_mut().set_attr(idx, "imagesrcset", &s),
3836                "imageSizes" => self.dom.borrow_mut().set_attr(idx, "imagesizes", &s),
3837                "loading" => self.dom.borrow_mut().set_attr(idx, "loading", &s),
3838                "decoding" => self.dom.borrow_mut().set_attr(idx, "decoding", &s),
3839                "allow" => self.dom.borrow_mut().set_attr(idx, "allow", &s),
3840                // `iframe.srcdoc`(読み出し側と対になる書き込み。
3841                // 2026-07-17 発見・実装)。
3842                "srcdoc" => self.dom.borrow_mut().set_attr(idx, "srcdoc", &s),
3843                // `crossOrigin = null` は属性除去(仕様どおり)、それ以外は文字列化して設定。
3844                "crossOrigin" => {
3845                    if matches!(val, Value::Null) {
3846                        self.dom.borrow_mut().remove_attr(idx, "crossorigin");
3847                    } else {
3848                        self.dom.borrow_mut().set_attr(idx, "crossorigin", &s);
3849                    }
3850                }
3851                "fetchPriority" => self.dom.borrow_mut().set_attr(idx, "fetchpriority", &s),
3852                "integrity" => self.dom.borrow_mut().set_attr(idx, "integrity", &s),
3853                "async" => {
3854                    let mut dom = self.dom.borrow_mut();
3855                    if val.truthy() {
3856                        dom.set_attr(idx, "async", "async");
3857                    } else {
3858                        dom.remove_attr(idx, "async");
3859                    }
3860                }
3861                "defer" => {
3862                    let mut dom = self.dom.borrow_mut();
3863                    if val.truthy() {
3864                        dom.set_attr(idx, "defer", "defer");
3865                    } else {
3866                        dom.remove_attr(idx, "defer");
3867                    }
3868                }
3869                "noModule" => {
3870                    let mut dom = self.dom.borrow_mut();
3871                    if val.truthy() {
3872                        dom.set_attr(idx, "nomodule", "nomodule");
3873                    } else {
3874                        dom.remove_attr(idx, "nomodule");
3875                    }
3876                }
3877                "href" => self.dom.borrow_mut().set_attr(idx, "href", &s),
3878                // `a`/`area.protocol`/`.host`/`.hostname`/`.port`/`.pathname`/
3879                // `.search`/`.hash`(`URLUtils`ミックスインの書き込み側。
3880                // ゲッターは既に`location_components`経由で実装済み
3881                // だったが、対になるセッターが丸ごと未対応だった。
3882                // `location.protocol`等の再構築ロジックと同じ「現在の
3883                // href を分解 → 対象コンポーネントだけ差し替え → 再結合
3884                // して href へ書き戻す」パターンを適用する。2026-07-18
3885                // 発見・実装)。
3886                "protocol" | "host" | "hostname" | "port" | "pathname" | "search" | "hash"
3887                | "username" | "password"
3888                    if self
3889                        .dom
3890                        .borrow()
3891                        .nodes
3892                        .get(idx)
3893                        .map(|n| n.tag == "a" || n.tag == "area")
3894                        .unwrap_or(false) =>
3895                {
3896                    let raw = self.dom.borrow().get_attr(idx, "href").unwrap_or_default();
3897                    let abs = self.resolve_url_against_page(&raw);
3898                    let comps = super::super::builtins::location_components(&abs);
3899                    let get = |k: &str| {
3900                        comps.iter().find(|(kk, _)| *kk == k).map(|(_, v)| v.clone()).unwrap_or_default()
3901                    };
3902                    let mut protocol = get("protocol");
3903                    let mut hostname = get("hostname");
3904                    let mut port = get("port");
3905                    let mut pathname = get("pathname");
3906                    let mut search = get("search");
3907                    let mut hash = get("hash");
3908                    let mut username = get("username");
3909                    let mut password = get("password");
3910                    match key {
3911                        "protocol" => protocol = if s.ends_with(':') { s.clone() } else { alloc::format!("{}:", s) },
3912                        "hostname" => hostname = s.clone(),
3913                        "port" => port = s.clone(),
3914                        "host" => match s.split_once(':') {
3915                            Some((h, p)) => {
3916                                hostname = String::from(h);
3917                                port = String::from(p);
3918                            }
3919                            None => {
3920                                hostname = s.clone();
3921                                port = String::new();
3922                            }
3923                        },
3924                        "pathname" => pathname = if s.starts_with('/') { s.clone() } else { alloc::format!("/{}", s) },
3925                        "search" => {
3926                            search = if s.is_empty() || s.starts_with('?') { s.clone() } else { alloc::format!("?{}", s) }
3927                        }
3928                        "hash" => {
3929                            hash = if s.is_empty() || s.starts_with('#') { s.clone() } else { alloc::format!("#{}", s) }
3930                        }
3931                        "username" => username = s.clone(),
3932                        "password" => password = s.clone(),
3933                        _ => {}
3934                    }
3935                    let hostport =
3936                        if port.is_empty() { hostname.clone() } else { alloc::format!("{}:{}", hostname, port) };
3937                    let userinfo = if username.is_empty() && password.is_empty() {
3938                        String::new()
3939                    } else {
3940                        alloc::format!("{}:{}@", username, password)
3941                    };
3942                    let new_href =
3943                        alloc::format!("{}//{}{}{}{}{}", protocol, userinfo, hostport, pathname, search, hash);
3944                    self.dom.borrow_mut().set_attr(idx, "href", &new_href);
3945                }
3946                "htmlFor" => self.dom.borrow_mut().set_attr(idx, "for", &s),
3947                // `form.acceptCharset`(読み出し側と対になる書き込み。
3948                // 2026-07-17 発見・実装)。
3949                "acceptCharset" => self.dom.borrow_mut().set_attr(idx, "accept-charset", &s),
3950                // `meta.httpEquiv`/`.content`/`.charset`(読み出し側と対になる
3951                // 書き込み。2026-07-17 発見・実装)。
3952                "httpEquiv" => self.dom.borrow_mut().set_attr(idx, "http-equiv", &s),
3953                "content" => self.dom.borrow_mut().set_attr(idx, "content", &s),
3954                "charset" => self.dom.borrow_mut().set_attr(idx, "charset", &s),
3955                // `blockquote`/`q`/`ins`/`del.cite`と`ins`/`del`/`time.dateTime`
3956                // (読み出し側と対になる書き込み。2026-07-17 発見・実装)。
3957                "cite" => self.dom.borrow_mut().set_attr(idx, "cite", &s),
3958                "dateTime" => self.dom.borrow_mut().set_attr(idx, "datetime", &s),
3959                "action" => self.dom.borrow_mut().set_attr(idx, "action", &s),
3960                "method" => self.dom.borrow_mut().set_attr(idx, "method", &s),
3961                // `form.enctype`/`.encoding`(読み出し側と対になる書き込み。
3962                // 両方とも同じ`enctype`属性を指す。2026-07-17 発見・実装)。
3963                "enctype" | "encoding" => self.dom.borrow_mut().set_attr(idx, "enctype", &s),
3964                // `formAction`/`formEnctype`/`formMethod`/`formTarget`(読み出し側と
3965                // 対になる書き込み。自身の`form*`属性に反映する。2026-07-17 発見・実装)。
3966                "formAction" => self.dom.borrow_mut().set_attr(idx, "formaction", &s),
3967                "formEnctype" => self.dom.borrow_mut().set_attr(idx, "formenctype", &s),
3968                "formMethod" => self.dom.borrow_mut().set_attr(idx, "formmethod", &s),
3969                "formTarget" => self.dom.borrow_mut().set_attr(idx, "formtarget", &s),
3970                "min" => self.dom.borrow_mut().set_attr(idx, "min", &s),
3971                "max" => self.dom.borrow_mut().set_attr(idx, "max", &s),
3972                // `meter.low`/`.high`/`.optimum`(読み出し側と対になる書き込み。
3973                // 2026-07-17 発見・実装)。
3974                "low" => self.dom.borrow_mut().set_attr(idx, "low", &s),
3975                "high" => self.dom.borrow_mut().set_attr(idx, "high", &s),
3976                "optimum" => self.dom.borrow_mut().set_attr(idx, "optimum", &s),
3977                // `td`/`th.colSpan`/`.rowSpan`(読み出し側と対になる書き込み。
3978                // 2026-07-17 発見・実装)。
3979                "colSpan" => self.dom.borrow_mut().set_attr(idx, "colspan", &s),
3980                "rowSpan" => self.dom.borrow_mut().set_attr(idx, "rowspan", &s),
3981                // `td`/`th.headers`/`th.scope`/`.abbr`(読み出し側と対になる
3982                // 書き込み。2026-07-17 発見・実装)。
3983                "headers" => self.dom.borrow_mut().set_attr(idx, "headers", &s),
3984                "scope" => self.dom.borrow_mut().set_attr(idx, "scope", &s),
3985                "abbr" => self.dom.borrow_mut().set_attr(idx, "abbr", &s),
3986                // `col`/`colgroup.span`(読み出し側と対になる書き込み。
3987                // 2026-07-17 発見・実装)。
3988                "span" => self.dom.borrow_mut().set_attr(idx, "span", &s),
3989                "step" => self.dom.borrow_mut().set_attr(idx, "step", &s),
3990                "maxLength" => self.dom.borrow_mut().set_attr(idx, "maxlength", &s),
3991                // `minLength`(`maxLength`と対になる書き込み側。ゲッターと同じく
3992                // 丸ごと未対応だった)。
3993                "minLength" => self.dom.borrow_mut().set_attr(idx, "minlength", &s),
3994                "target" => self.dom.borrow_mut().set_attr(idx, "target", &s),
3995                "rel" => self.dom.borrow_mut().set_attr(idx, "rel", &s),
3996                "download" => self.dom.borrow_mut().set_attr(idx, "download", &s),
3997                // `area.coords`/`.shape`(読み出し側と対になる書き込み。
3998                // 2026-07-17 発見・実装)。
3999                "coords" => self.dom.borrow_mut().set_attr(idx, "coords", &s),
4000                "shape" => self.dom.borrow_mut().set_attr(idx, "shape", &s),
4001                "lang" => self.dom.borrow_mut().set_attr(idx, "lang", &s),
4002                "dir" => self.dom.borrow_mut().set_attr(idx, "dir", &s),
4003                // `element.title`/`.accessKey`(読み出し側と対になる書き込み。
4004                // 2026-07-16 発見・実装)。
4005                "title" => self.dom.borrow_mut().set_attr(idx, "title", &s),
4006                "accessKey" => self.dom.borrow_mut().set_attr(idx, "accesskey", &s),
4007                // `element.translate = true/false`(`"yes"`/`"no"` へ反映する
4008                // 仕様どおりのブール属性)。
4009                "translate" => {
4010                    let v = if val.truthy() { "yes" } else { "no" };
4011                    self.dom.borrow_mut().set_attr(idx, "translate", v);
4012                }
4013                // `element.draggable = true/false`(`"true"`/`"false"` へ反映
4014                // する簡略ブール属性。読み出し側と対)。
4015                "draggable" => {
4016                    let v = if val.truthy() { "true" } else { "false" };
4017                    self.dom.borrow_mut().set_attr(idx, "draggable", v);
4018                }
4019                // `element.slot`(読み出し側と対になる書き込み)。
4020                "slot" => self.dom.borrow_mut().set_attr(idx, "slot", &s),
4021                // `element.contentEditable = "true"/"false"/"plaintext-only"`(無効値は
4022                // 仕様上 `SyntaxError` だが、この setter は `Result` を返さない既存の
4023                // 属性直結プロパティ群と同じ形のため、他の簡略実装同様エラーは投げず
4024                // 無効値は無視する)。
4025                "contentEditable" => {
4026                    if matches!(s.as_str(), "true" | "false" | "plaintext-only") {
4027                        self.dom.borrow_mut().set_attr(idx, "contenteditable", &s);
4028                    }
4029                }
4030
4031                "tabIndex" => {
4032                    let n = val.to_number() as i64;
4033                    self.dom.borrow_mut().set_attr(idx, "tabindex", &n.to_string());
4034                }
4035                // `element.spellcheck = true/false`(`contentEditable` と同じ
4036                // 属性直結の簡略実装)。
4037                "spellcheck" => {
4038                    let b = val.truthy();
4039                    self.dom.borrow_mut().set_attr(idx, "spellcheck", if b { "true" } else { "false" });
4040                }
4041                "inputMode" => self.dom.borrow_mut().set_attr(idx, "inputmode", &s),
4042                // `select`/`input.size`(読み出し側と対になる書き込み。
4043                // 2026-07-18 発見・実装)。
4044                "size" => self.dom.borrow_mut().set_attr(idx, "size", &s),
4045                // `textarea.rows`/`.cols`(読み出し側と対になる書き込み。
4046                // 2026-07-18 発見・実装)。
4047                "rows" => self.dom.borrow_mut().set_attr(idx, "rows", &s),
4048                "cols" => self.dom.borrow_mut().set_attr(idx, "cols", &s),
4049                // `autocomplete`/`autofocus`(読み出し側と対になる書き込み。
4050                // 2026-07-18 発見・実装)。
4051                "autocomplete" => self.dom.borrow_mut().set_attr(idx, "autocomplete", &s),
4052                "autofocus" => {
4053                    let mut dom = self.dom.borrow_mut();
4054                    if val.truthy() {
4055                        dom.set_attr(idx, "autofocus", "autofocus");
4056                    } else {
4057                        dom.remove_attr(idx, "autofocus");
4058                    }
4059                }
4060                "enterKeyHint" => self.dom.borrow_mut().set_attr(idx, "enterkeyhint", &s),
4061                "autocapitalize" => self.dom.borrow_mut().set_attr(idx, "autocapitalize", &s),
4062                // `element.popover = null` は属性除去(仕様どおり)、それ以外は文字列化して設定。
4063                "popover" => {
4064                    if matches!(val, Value::Null) {
4065                        self.dom.borrow_mut().remove_attr(idx, "popover");
4066                    } else {
4067                        self.dom.borrow_mut().set_attr(idx, "popover", &s);
4068                    }
4069                }
4070                "popoverTargetAction" => {
4071                    self.dom.borrow_mut().set_attr(idx, "popovertargetaction", &s)
4072                }
4073                // `button.popoverTargetElement = el`(`null` で除去)。この処理系は
4074                // `popovertarget` を idref 文字列としてしか保持できないため、対象
4075                // 要素が `id` を持つ場合のみ `popovertarget` へその id を反映する
4076                // 簡略実装(`id` の無い要素を渡すケースは対象外)。
4077                "popoverTargetElement" => match val {
4078                    Value::Null => {
4079                        self.dom.borrow_mut().remove_attr(idx, "popovertarget");
4080                    }
4081                    Value::Object(o) if matches!(&o.borrow().kind, ObjKind::DomElement(_)) => {
4082                        if let ObjKind::DomElement(ti) = &o.borrow().kind {
4083                            let target_id = self.dom.borrow().nodes.get(*ti).map(|n| n.id.clone());
4084                            if let Some(tid) = target_id.filter(|s| !s.is_empty()) {
4085                                self.dom.borrow_mut().set_attr(idx, "popovertarget", &tid);
4086                            }
4087                        }
4088                    }
4089                    _ => {}
4090                },
4091                // `button.command`/`.commandForElement`(Invoker Commands API。
4092                // `popoverTargetAction`/`.popoverTargetElement`と全く同じ
4093                // 反映・idref解決パターン。2026-07-18 発見・実装)。
4094                "command" => self.dom.borrow_mut().set_attr(idx, "command", &s),
4095                "commandForElement" => match val {
4096                    Value::Null => {
4097                        self.dom.borrow_mut().remove_attr(idx, "commandfor");
4098                    }
4099                    Value::Object(o) if matches!(&o.borrow().kind, ObjKind::DomElement(_)) => {
4100                        if let ObjKind::DomElement(ti) = &o.borrow().kind {
4101                            let target_id = self.dom.borrow().nodes.get(*ti).map(|n| n.id.clone());
4102                            if let Some(tid) = target_id.filter(|s| !s.is_empty()) {
4103                                self.dom.borrow_mut().set_attr(idx, "commandfor", &tid);
4104                            }
4105                        }
4106                    }
4107                    _ => {}
4108                },
4109                // `table.tHead`/`.tFoot`/`.caption`(読み出し側は既に実装
4110                // 済みだったが、対になる書き込み側が丸ごと未対応だった。
4111                // 仕様上、代入すると既存の同名セクションを除去し、新しい
4112                // 要素(`null`なら除去のみ)を仕様上の正しい位置
4113                // (`thead`/`caption`は先頭子、`tfoot`は末尾)へ挿入する。
4114                // `insert_before`は内部で対象ノードを自動的に元の親から
4115                // detachするため、既にDOM内にある要素を渡しても正しく
4116                // 再配置される。2026-07-18 発見・実装)。
4117                "tHead" | "tFoot" | "caption"
4118                    if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "table").unwrap_or(false) =>
4119                {
4120                    let want = match key {
4121                        "tHead" => "thead",
4122                        "tFoot" => "tfoot",
4123                        _ => "caption",
4124                    };
4125                    let existing = {
4126                        let dom = self.dom.borrow();
4127                        dom.nodes[idx].children.iter().find(|&&c| dom.nodes[c].tag == want).copied()
4128                    };
4129                    if let Some(old) = existing {
4130                        self.dom.borrow_mut().remove_node(old);
4131                    }
4132                    if let Value::Object(o) = &val {
4133                        if let ObjKind::DomElement(new_idx) = &o.borrow().kind {
4134                            let new_idx = *new_idx;
4135                            let ref_child = if want == "tfoot" {
4136                                None
4137                            } else {
4138                                self.dom.borrow().nodes[idx].children.first().copied()
4139                            };
4140                            self.dom.borrow_mut().insert_before(idx, new_idx, ref_child);
4141                        }
4142                    }
4143                }
4144                "scrollTop" => {
4145                    // JS が el.scrollTop を書き込んだ → scroll_tops に反映して再描画を要求。
4146                    let n = val.to_number() as i32;
4147                    let mut dom = self.dom.borrow_mut();
4148                    dom.scroll_tops.insert(idx, n.max(0));
4149                    dom.dirty = true;
4150                }
4151                "scrollLeft" => {
4152                    // JS が el.scrollLeft を書き込んだ → scroll_lefts に反映して再描画を要求。
4153                    let n = val.to_number() as i32;
4154                    let mut dom = self.dom.borrow_mut();
4155                    dom.scroll_lefts.insert(idx, n.max(0));
4156                    dom.dirty = true;
4157                }
4158                // ARIA 反映 IDL 属性の setter。`null`/`undefined` 代入は仕様どおり
4159                // 属性そのものを取り除く(`""` を設定するのとは異なる)。
4160                key if aria_attr_name(key).is_some() => {
4161                    let attr = aria_attr_name(key).unwrap_or_default();
4162                    let mut dom = self.dom.borrow_mut();
4163                    if matches!(val, Value::Null | Value::Undefined) {
4164                        dom.remove_attr(idx, attr);
4165                    } else {
4166                        dom.set_attr(idx, attr, &s);
4167                    }
4168                }
4169                // 【2026-07-23 発見・修正】以前は`Obj::dom(idx)`(呼び出す度に使い捨てで
4170                // 新規生成される、どこにも永続化されないラッパーオブジェクト)自身の
4171                // `.props`へ書き込んでいたため、代入直後にそのRcが解放されると値ごと
4172                // 消え、`dispatchEvent`(`Obj::dom(idx)`を新たに生成して読む)側からは
4173                // 常に見つからず、`element.onchange = fn`のような関数値代入が一切発火
4174                // しない静かなバグだった。`listeners`(addEventListener系)と同じく
4175                // DOM構造体側の永続ストレージ`on_handlers`へ書く。
4176                // `canvas.width` / `canvas.height` への代入。
4177                //
4178                // 以前は下の `_ => {}` に落ちて**完全に捨てられていた**ため、
4179                // 大きさを変えても面はそのままだった。仕様では、
4180                // 同じ値を入れ直しても内容と描画状態が初期化される
4181                // (`canvas.width = canvas.width` という消去の定型句が
4182                // これに依存している)。
4183                "width" | "height" if self.dom.borrow().nodes.get(idx)
4184                    .map(|n| n.tag == "canvas").unwrap_or(false) =>
4185                {
4186                    self.dom.borrow_mut().set_attr(idx, key, &s);
4187                    let id = self
4188                        .dom
4189                        .borrow()
4190                        .get_attr(idx, "_c2d_id")
4191                        .and_then(|v| v.parse::<u32>().ok());
4192                    if let Some(id) = id {
4193                        let get = |k: &str, d: u32| {
4194                            self.dom
4195                                .borrow()
4196                                .get_attr(idx, k)
4197                                .and_then(|v| v.trim().parse::<u32>().ok())
4198                                .unwrap_or(d)
4199                        };
4200                        let (w, h) = (get("width", 300), get("height", 150));
4201                        crate::os_lib::canvas2d::resize_context(id, w, h);
4202                    }
4203                }
4204                // `node.__proto__ = X.prototype`: 原型は要素側に持たせる(ハンドルは作り直すため)。
4205                // 【2026-10-20】ShadyDOM は DocumentFragment を `__proto__ = ShadowRoot.prototype`
4206                // にしてから `b.Qa(…)` を呼ぶ(YouTube の Polymer の attachShadow)。
4207                "__proto__" => {
4208                    if let Value::Object(p) = &val {
4209                        self.dom.borrow_mut().node_protos.insert(idx, p.clone());
4210                    }
4211                }
4212                _ if key.starts_with("on") && key.len() > 2 => {
4213                    self.dom.borrow_mut().set_on_handler(idx, key, val.clone());
4214                    if let Value::Str(s_val) = &val {
4215                        self.dom.borrow_mut().set_attr(idx, key, s_val);
4216                    }
4217                }
4218                // それ以外は要素の独自プロパティとして覚える(`DomBridge::expandos`)。
4219                // ただし原型の連鎖に setter があればそれを呼ぶ(カスタム要素のクラスの
4220                // プロパティ。Polymer はプロパティを prototype のアクセサとして定義する)。
4221                _ => {
4222                    let own = self.dom.borrow().expandos.get(&idx).is_some_and(|m| m.contains_key(key));
4223                    if !own {
4224                        let mut cur = self.get_dom_node_proto(idx);
4225                        let mut guard = 0;
4226                        while let Some(p) = cur {
4227                            let acc = p.borrow().accessors.get(key).map(|a| a.set.clone());
4228                            if let Some(setter) = acc {
4229                                if let Some(f) = setter {
4230                                    let _ = self.call_value(&f, Value::Object(Obj::dom(idx)), &[val]);
4231                                }
4232                                return;
4233                            }
4234                            if p.borrow().props.contains_key(key) {
4235                                break;
4236                            }
4237                            guard += 1;
4238                            if guard > 64 {
4239                                break;
4240                            }
4241                            cur = p.borrow().proto.clone();
4242                        }
4243                    }
4244                    self.dom
4245                        .borrow_mut()
4246                        .expandos
4247                        .entry(idx)
4248                        .or_default()
4249                        .insert(String::from(key), val);
4250                }
4251            },
4252            DomDisp::Host(tag) => {
4253                if let Some(rest) = tag.strip_prefix("style:") {
4254                    let idx: usize = rest.parse().unwrap_or(usize::MAX);
4255                    if key == "cssText" {
4256                        self.dom.borrow_mut().set_css_text(idx, &s);
4257                    } else {
4258                        // `style.cssFloat`(読み出し側と対になる書き込み。
4259                        // 2026-07-17 発見・実装)。
4260                        let key = if key == "cssFloat" { "float" } else { key };
4261                        self.dom.borrow_mut().set_style(idx, key, &s);
4262                    }
4263                } else if let Some(rest) = tag.strip_prefix("dataset:") {
4264                    let idx: usize = rest.parse().unwrap_or(usize::MAX);
4265                    let attr = camel_to_data_attr(key);
4266                    self.dom.borrow_mut().set_attr(idx, &attr, &s);
4267                } else if let Some(rest) = tag.strip_prefix("classList:") {
4268                    let idx: usize = rest.parse().unwrap_or(usize::MAX);
4269                    if key == "value" {
4270                        self.dom.borrow_mut().set_attr(idx, "class", &s);
4271                    }
4272                }
4273            }
4274        }
4275    }
4276}
4277
4278pub(crate) fn template_fragment_clone_node(
4279    it: &mut Interp,
4280    this: Value,
4281    args: &[Value],
4282) -> Result<Value, Value> {
4283    let deep = args.get(0).map(|v| v.truthy()).unwrap_or(false);
4284    let new_frag = Obj::plain();
4285    new_frag.borrow_mut().props.insert("nodeType".into(), Value::Number(11.0));
4286    new_frag.borrow_mut().props.insert("nodeName".into(), Value::str("#document-fragment"));
4287
4288    if let Value::Object(o) = &this {
4289        let child_vals = if let Some(Value::Object(arr_obj)) = o.borrow().props.get("childNodes") {
4290            if let ObjKind::Array(elems) = &arr_obj.borrow().kind {
4291                elems.clone()
4292            } else {
4293                alloc::vec::Vec::new()
4294            }
4295        } else {
4296            alloc::vec::Vec::new()
4297        };
4298
4299        let mut cloned_children = alloc::vec::Vec::new();
4300        for child_val in child_vals {
4301            if let Value::Object(co) = &child_val {
4302                if let ObjKind::DomElement(c_idx) = co.borrow().kind {
4303                    let cloned_idx = it.dom.borrow_mut().clone_node(c_idx, deep);
4304                    cloned_children.push(Value::Object(Obj::dom(cloned_idx)));
4305                    continue;
4306                }
4307            }
4308            cloned_children.push(child_val.clone());
4309        }
4310
4311        let cloned_arr = Obj::array(cloned_children);
4312        new_frag.borrow_mut().props.insert("childNodes".into(), Value::Object(cloned_arr.clone()));
4313        new_frag.borrow_mut().props.insert("children".into(), Value::Object(cloned_arr));
4314    }
4315    let clone_fn = Obj::native("cloneNode", template_fragment_clone_node);
4316    new_frag.borrow_mut().props.insert("cloneNode".into(), Value::Object(clone_fn));
4317
4318    Ok(Value::Object(new_frag))
4319}