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