Skip to main content

atmos/os_lib/js/
dom_bridge.rs

1//! JS ⇄ レンダラの DOM ブリッジ。
2//!
3//! `dom::parse_html` の結果を **ドキュメント順** にフラット化した軽量アリーナを持ち、
4//! JS から `document.getElementById` 等で参照されるノードと、`textContent` /
5//! `style` / `classList` の変更、`addEventListener('click')` で登録されたリスナを保持する。
6//!
7//! レンダラは毎レイアウトで `last_html` を再パースするため、ブリッジは
8//! 「同一 HTML を同一ドキュメント順で歩けばインデックスが一致する」性質を使い、
9//! `apply_overrides` で再パース木へ JS の変更を焼き戻してから CSS カスケードに渡す。
10//! これにより既存のレンダリング経路をそのまま再利用できる(HTML 再シリアライズ不要)。
11
12use alloc::collections::BTreeMap;
13use alloc::collections::BTreeSet;
14use alloc::format;
15use alloc::string::String;
16use alloc::string::ToString;
17use alloc::vec::Vec;
18
19use super::value::Value;
20use crate::os_lib::dom::{Node, NodeType};
21
22/// ブリッジ上の 1 ノード。要素またはテキスト。
23pub struct DomNode {
24    /// 要素タグ(小文字)。テキストノードは空。
25    pub tag: String,
26    pub is_text: bool,
27    pub id: String,
28    pub classes: Vec<String>,
29    /// インライン style(プロパティ→値)。
30    pub style: BTreeMap<String, String>,
31    /// その他の属性(id/class/style 以外も含む生の値)。
32    pub attrs: BTreeMap<String, String>,
33    /// テキストノードの内容、または textContent 上書き値。
34    pub text_override: Option<String>,
35    /// 構築時のテキスト(textContent ゲッタ用)。
36    pub initial_text: String,
37    pub parent: Option<usize>,
38    pub children: Vec<usize>,
39    /// JS が classes を変更したか(apply で焼き戻す)。
40    pub classes_dirty: bool,
41    /// JS が style を変更したか。
42    pub style_dirty: bool,
43}
44
45/// MutationObserver の登録エントリ。
46pub struct MutObsReg {
47    pub id: u32,
48    pub target_idx: usize,
49    pub child_list: bool,
50    pub attributes: bool,
51    pub subtree: bool,
52    pub callback: Value,
53    /// `observe(target, {attributeOldValue: true})`。変更前の属性値を
54    /// レコードの `oldValue` として公開するかどうか。
55    pub attribute_old_value: bool,
56    /// `observe(target, {attributeFilter: [...]})`。指定時はこの一覧に
57    /// 含まれる属性名の変化のみ通知する(`None` は全属性を監視)。
58    pub attribute_filter: Option<Vec<String>>,
59    /// `observe(target, {characterData: true})`(丸ごと未対応だった。
60    /// テキストノードの`.data`/`.nodeValue`/`textContent`変更を監視する
61    /// オプション自体が存在せず、対応する通知ヘルパ〔`notify_character_
62    /// data`〕も無かったため、`characterData`を指定してもコールバックが
63    /// 一切呼ばれなかった。`childList`/`attributes`は既に対応済みで
64    /// `characterData`だけ抜けていた兄弟ギャップ。2026-07-17 発見・実装)。
65    pub character_data: bool,
66    /// `observe(target, {characterDataOldValue: true})`。変更前のテキスト
67    /// 値をレコードの`oldValue`として公開するかどうか(`attributeOldValue`
68    /// と対)。
69    pub character_data_old_value: bool,
70}
71
72/// MutationObserver に届けるレコード(Rust 側内部表現)。
73pub struct MutRec {
74    pub type_: &'static str, // "childList" | "attributes"
75    pub target_idx: usize,
76    pub added: Vec<usize>,
77    pub removed: Vec<usize>,
78    pub attribute_name: Option<String>,
79    /// `attributeOldValue: true` が指定された観察者向けにのみ Some を積む。
80    pub old_value: Option<String>,
81}
82
83/// click 等のイベントリスナ登録。
84pub struct Listener {
85    pub node: usize,
86    pub event: String,
87    pub func: Value,
88    /// キャプチャフェーズで発火するリスナか(addEventListener の capture オプション)。
89    pub capture: bool,
90    /// 一度発火したら自動削除されるか(once オプション)。
91    pub once: bool,
92    /// 一意 ID(once 削除や removeEventListener の照合に使う)。
93    pub id: u64,
94    /// `addEventListener(type, fn, {signal})`(丸ごと未対応だった。
95    /// `AbortController` でリスナを一括解除する定番パターン。2026-07-14
96    /// 発見・実装)。`signal.aborted` を都度チェックし、abort 済みなら
97    /// 発火しない簡略実装(`abort` イベント発火と同時に即座に物理削除は
98    /// せず、次回参照時に遅延評価する。実務上は「以後発火しなくなる」
99    /// という観測可能な挙動が同じであれば十分なため)。
100    pub signal: Option<Value>,
101}
102
103/// `AbortSignal` オブジェクトの `.aborted` が `true` か。
104pub fn is_signal_aborted(signal: &Value) -> bool {
105    matches!(signal, Value::Object(o) if matches!(o.borrow().props.get("aborted"), Some(Value::Bool(true))))
106}
107
108fn is_listener_aborted(l: &Listener) -> bool {
109    l.signal.as_ref().is_some_and(is_signal_aborted)
110}
111
112/// DOM ブリッジ本体。`JsRuntime` が `Rc<RefCell<>>` で保持し、レンダラと共有する。
113pub struct DomBridge {
114    pub nodes: Vec<DomNode>,
115    pub listeners: Vec<Listener>,
116    /// JS が DOM を変更したフラグ。ホストはこれを見て再レイアウトする。
117    pub dirty: bool,
118    /// 構築済みかどうか(未構築なら getElementById 等は空を返す)。
119    pub built: bool,
120    /// レイアウト後の要素矩形 (x, y, w, h)(ページ座標)。getBoundingClientRect /
121    /// offsetWidth 等が参照。レンダラが flatten 後に populate する。
122    pub rects: BTreeMap<usize, (i32, i32, i32, i32)>,
123    /// レイアウト後の各要素のborder幅 (top, right, bottom, left)。
124    pub border_widths: BTreeMap<usize, (i32, i32, i32, i32)>,
125    /// 算出スタイル(カスケード後の specified_values、kebab-case キー)。
126    /// getComputedStyle が参照。レンダラがスタイル適用後に populate する。
127    pub computed_styles: BTreeMap<usize, BTreeMap<String, String>>,
128    /// サブスクロールコンテナの現在 scrollTop (node_idx → px)。
129    /// JS が `el.scrollTop = N` で書き込み、レンダラが再レイアウト後に同期する。
130    pub scroll_tops: BTreeMap<usize, i32>,
131    /// サブスクロールコンテナの scrollHeight (node_idx → px)。
132    /// レンダラが parse_and_layout 後に populate する(コンテナ高 + scroll_max_y)。
133    pub scroll_heights: BTreeMap<usize, i32>,
134    /// リスナ ID の発番カウンタ(once / removeEventListener 照合用)。
135    pub next_listener_id: u64,
136    /// 登録済み MutationObserver。
137    pub mut_observers: Vec<MutObsReg>,
138    /// DOM 変更時に積まれるレコードキュー(observer_id, record)。
139    pub mut_records: Vec<(u32, MutRec)>,
140    /// MutationObserver の ID 発番カウンタ。
141    pub next_mut_obs_id: u32,
142    /// `setPointerCapture`/`releasePointerCapture`/`hasPointerCapture`
143    /// (Pointer Events。丸ごと未対応だった)が捕捉中の (node_idx, pointerId) 集合。
144    /// この処理系のイベント配信はヒットテストベースのため、実際にその後の
145    /// pointer イベントをこの要素へ強制配送するところまでは配線しない
146    /// (`hasPointerCapture` が仕様どおりの真偽を返せるようにする状態管理のみの
147    /// 簡略実装。`inert` 属性のポインタ操作抑制のみ実装と同種の部分対応)。
148    pub pointer_captures: BTreeSet<(usize, i32)>,
149    /// `document.activeElement`/`element.focus()`/`.blur()`(HTML5。丸ごと
150    /// 未対応だった)。現在フォーカス中のノード index。`WebEngine::focused_id`
151    /// (レンダラ側の文字列キー版フォーカス状態)とは別に、JS 側から直接
152    /// 読み書きできる node index ベースの状態としてここに持つ。レンダラの
153    /// 実クリック/`autofocus` によるフォーカス変更も同期される(詳細は
154    /// `layout.rs`/`render.rs` の `focused_id` 代入箇所を参照)。id/name 属性を
155    /// 持たない要素へのレンダラ起点フォーカスは同期対象外(レンダラ自身の
156    /// 既存の制約と同じ)。
157    pub focused_idx: Option<usize>,
158    /// `window.scrollTo(x, y)`/`window.scroll(x, y)`(丸ごと未対応だった。
159    /// 実装はあったが `SCROLL_Y` という孤立した `static` へ書き込むのみで、
160    /// `window.scrollY`/実際のレンダラ描画位置のどちらにも一切反映されない
161    /// 死んだ経路だった。2026-07-16 発見)。JS→レンダラの片方向同期用の
162    /// 「保留中の絶対スクロール先」。`focused_idx`と同じ「レンダラが毎フレーム
163    /// 消費する」設計だが、フォーカスと違い同一フレーム内での即時反映が
164    /// 不要(次の描画で反映されれば十分)なため、`render.rs`の`draw()`冒頭
165    /// 1箇所でのみ消費すればよく、`.focus()`のような10箇所以上への配線は
166    /// 不要(改修規模がずっと小さい)。
167    pub pending_scroll_abs_y: Option<i32>,
168    /// `window.scrollBy(x, y)`(丸ごと未対応だった。`dom_noop`のまま)。
169    /// 複数回呼ばれた場合は加算し、次の描画で一括反映する。
170    pub pending_scroll_by_y: Option<i32>,
171    /// `element.onchange = fn`のような GlobalEventHandlers IDL 属性(関数値)の
172    /// 永続的な保管先。以前は`Obj::dom(idx)`(呼び出す度に新規生成される使い捨ての
173    /// ラッパーオブジェクト)自身の`.props`へ書き込んでいたため、代入直後に
174    /// そのRcが破棄されると値ごと消え、`dispatchEvent`側が同じ`Obj::dom(idx)`を
175    /// 新規生成して読み直しても常に見つからない(=ハンドラが一切発火しない)
176    /// 静かなバグだった。`listeners`(addEventListener系)と同じくDOM構造体側に
177    /// 持たせることで、どの`Obj::dom(idx)`インスタンス経由でも一貫して読み書き
178    /// できるようにする。キーは`(node_idx, "onchange"等のプロパティ名)`。
179    pub on_handlers: BTreeMap<(usize, String), Value>,
180}
181
182impl Default for DomBridge {
183    fn default() -> Self {
184        Self::new()
185    }
186}
187
188/// `ValidityState`(HTML5 制約検証)の個別フラグ。以前は `element.validity` が
189/// `valid`/`valueMissing`/`customError` の3つしか公開せず、`input.validity.
190/// patternMismatch`/`.typeMismatch`/`.rangeOverflow` 等、特定の制約だけを狙い撃ちして
191/// カスタムエラーメッセージを出し分ける定番パターンが常に `undefined`(falsy)に
192/// なっていた。
193#[derive(Default)]
194pub struct ValidityFlags {
195    pub value_missing: bool,
196    pub type_mismatch: bool,
197    pub pattern_mismatch: bool,
198    pub too_long: bool,
199    pub too_short: bool,
200    pub range_overflow: bool,
201    pub range_underflow: bool,
202    pub step_mismatch: bool,
203    pub bad_input: bool,
204    pub custom_error: bool,
205}
206impl ValidityFlags {
207    pub fn valid(&self) -> bool {
208        !(self.value_missing
209            || self.type_mismatch
210            || self.pattern_mismatch
211            || self.too_long
212            || self.too_short
213            || self.range_overflow
214            || self.range_underflow
215            || self.step_mismatch
216            || self.bad_input
217            || self.custom_error)
218    }
219}
220
221// ===== セレクタパーサ(`matches`/`closest`/`querySelector(All)` 共通) =====
222// タグ/`#id`/`.class`/`[attr...]` のコンパウンドセレクタと、空白(子孫)/`>`(子)/
223// `+`(隣接兄弟)/`~`(一般兄弟)の結合子、`:not()`/`:is()`/`:where()`/
224// `:first-child`/`:last-child`/`:only-child`/`:nth-child()`/`:nth-last-child()`/
225// `:first-of-type`/`:last-of-type`/`:only-of-type`/`:nth-of-type()`/
226// `:nth-last-of-type()` 疑似クラスをサポートする。`:hover`/`:has()`/`:scope` 等
227// その他の疑似クラスは非対応(改修規模を絞るための意図的な割り切り)。
228
229#[derive(Clone, Copy)]
230enum AttrOp {
231    Exists,
232    Eq,
233    Prefix,
234    Suffix,
235    Contains,
236    Word,
237}
238
239struct AttrSel {
240    name: String,
241    op: AttrOp,
242    value: String,
243}
244
245#[derive(Default)]
246struct CompoundSel {
247    tag: Option<String>,
248    id: Option<String>,
249    classes: Vec<String>,
250    attrs: Vec<AttrSel>,
251    /// `:not(sel1, sel2, ...)` の中身(コンパウンドセレクタのみ対応。結合子を含む
252    /// 複雑なセレクタは非対応)。いずれか1つでも一致すれば不一致とする。
253    not: Vec<CompoundSel>,
254    /// `:is(sel1, sel2, ...)`/`:where(...)` の中身(コンパウンドセレクタのみ対応。
255    /// 仕様上 `:where()` は詳細度の扱いのみ `:is()` と異なるが、この処理系は
256    /// 詳細度を計算しないためマッチ判定は完全に同一として扱う)。いずれか1つでも
257    /// 一致すれば一致とする(空なら制約なし = 常に一致)。
258    is_list: Vec<CompoundSel>,
259    /// `:first-child`/`:last-child`/`:only-child`/`:nth-child(...)`。
260    pos: Vec<PosPseudo>,
261    /// `:first-of-type`/`:last-of-type`/`:only-of-type`/`:nth-of-type(...)`
262    /// (タグ名が同じ兄弟のみを数える点以外は `pos` と同じ規則)。
263    pos_of_type: Vec<PosPseudo>,
264    /// `:scope`(Selectors Level 4。丸ごと未対応だった)。マッチ時に呼び出し元が
265    /// 渡す `scope_root`(`element.querySelectorAll`/`matches`/`closest` 等の
266    /// 起点ノード)と一致する場合のみ真になる。詳細は `compound_matches` 参照。
267    is_scope: bool,
268}
269
270#[derive(Clone, Copy)]
271struct NthFormula {
272    a: i32,
273    b: i32,
274}
275
276#[derive(Clone, Copy)]
277enum PosPseudo {
278    First,
279    Last,
280    Only,
281    Nth(NthFormula),
282    /// `:nth-last-child()`/`:nth-last-of-type()`(末尾から数える版)。判定時に
283    /// `count - position + 1`(末尾からの1始まり位置)を式へ渡す点のみ `Nth` と異なる。
284    NthLast(NthFormula),
285}
286
287/// `:nth-child()` 引数(`odd`/`even`/`N`/`aN+b`/`aN-b`/`-aN+b`)を解析する。
288fn parse_nth_formula(s: &str) -> Option<NthFormula> {
289    let s: String = s.chars().filter(|c| !c.is_whitespace()).collect();
290    let s = s.to_lowercase();
291    if s == "odd" {
292        return Some(NthFormula { a: 2, b: 1 });
293    }
294    if s == "even" {
295        return Some(NthFormula { a: 2, b: 0 });
296    }
297    if let Some(n_pos) = s.find('n') {
298        let a_part = s.get(..n_pos).unwrap_or("");
299        let a = match a_part {
300            "" | "+" => 1,
301            "-" => -1,
302            _ => a_part.parse::<i32>().ok()?,
303        };
304        let b_part = s.get(n_pos + 1..).unwrap_or("");
305        let b = if b_part.is_empty() {
306            0
307        } else {
308            b_part.parse::<i32>().ok()?
309        };
310        return Some(NthFormula { a, b });
311    }
312    let n = s.parse::<i32>().ok()?;
313    Some(NthFormula { a: 0, b: n })
314}
315
316/// `position`(1始まり)が `an+b` 式(`n` は 0,1,2,...)のいずれかの `n` で
317/// 表せるか判定する。
318fn nth_formula_matches(f: NthFormula, position: usize) -> bool {
319    let p = position as i32;
320    if f.a == 0 {
321        return p == f.b;
322    }
323    let diff = p - f.b;
324    diff % f.a == 0 && diff / f.a >= 0
325}
326
327#[derive(Clone, Copy)]
328enum Combinator {
329    Descendant,
330    Child,
331    Adjacent,
332    General,
333}
334
335struct SelStep {
336    compound: CompoundSel,
337    /// 直前のステップとの結合子(先頭ステップは `None`)。
338    combinator: Option<Combinator>,
339}
340
341/// `[attr...]` 1件を解析する(`name`/`name=val`/`name^=val`/`name$=val`/`name*=val`/
342/// `name~=val`。値の前後のクォートは剥がす)。
343fn parse_attr_sel(inner: &str) -> Option<AttrSel> {
344    let inner = inner.trim();
345    for (token, op) in [
346        ("^=", AttrOp::Prefix),
347        ("$=", AttrOp::Suffix),
348        ("*=", AttrOp::Contains),
349        ("~=", AttrOp::Word),
350        ("=", AttrOp::Eq),
351    ] {
352        if let Some((name, val)) = inner.split_once(token) {
353            let val = val.trim().trim_matches(|c| c == '"' || c == '\'');
354            return Some(AttrSel {
355                name: name.trim().to_string(),
356                op,
357                value: val.to_string(),
358            });
359        }
360    }
361    if inner.is_empty() {
362        return None;
363    }
364    Some(AttrSel {
365        name: inner.to_string(),
366        op: AttrOp::Exists,
367        value: String::new(),
368    })
369}
370
371/// コンビネータを含まない単一コンパウンドセレクタ(`div.active#foo[data-x=y]`)を解析する。
372/// `*`(ユニバーサルセレクタ)は制約が何も無い `CompoundSel::default()` になるが、
373/// 末尾の「制約が空なら無効」判定に引っかかって `None` に潰れてしまい、
374/// `querySelectorAll('*')` が常に0件になる回帰があった(自己検証で発見)。
375/// `*` はここで即座に有効な(=全ノードにマッチする)セレクタとして扱う。
376fn parse_compound(s: &str) -> Option<CompoundSel> {
377    let s = s.trim();
378    if s.is_empty() {
379        return None;
380    }
381    if s == "*" {
382        return Some(CompoundSel::default());
383    }
384    let mut c = CompoundSel::default();
385    let mut cur = String::new();
386    let mut mode = ' '; // ' '=tag, '#'=id, '.'=class, '['=attr
387    let chars: Vec<char> = s.chars().collect();
388    let mut i = 0;
389    let flush = |mode: char, cur: &mut String, c: &mut CompoundSel| {
390        if cur.is_empty() {
391            return;
392        }
393        match mode {
394            ' ' => {
395                if cur.as_str() != "*" {
396                    c.tag = Some(cur.to_lowercase());
397                }
398            }
399            '#' => c.id = Some(cur.clone()),
400            '.' => c.classes.push(cur.clone()),
401            _ => {}
402        }
403        cur.clear();
404    };
405    while i < chars.len() {
406        let ch = chars[i];
407        match ch {
408            '#' | '.' => {
409                flush(mode, &mut cur, &mut c);
410                mode = ch;
411            }
412            '[' => {
413                flush(mode, &mut cur, &mut c);
414                let start = i + 1;
415                let mut depth = 1;
416                let mut j = start;
417                while j < chars.len() && depth > 0 {
418                    match chars[j] {
419                        '[' => depth += 1,
420                        ']' => depth -= 1,
421                        _ => {}
422                    }
423                    if depth > 0 {
424                        j += 1;
425                    }
426                }
427                let inner: String = chars[start..j.min(chars.len())].iter().collect();
428                if let Some(attr) = parse_attr_sel(&inner) {
429                    c.attrs.push(attr);
430                }
431                i = j;
432                mode = ' ';
433            }
434            ':' => {
435                // `:pseudo` / `:pseudo(...)`。`:not(sel1, sel2, ...)`/`:nth-child(...)`/
436                // `:first-child`/`:last-child`/`:only-child` に対応。その他の疑似クラス
437                // (`:is()`/`:where()`/`:hover` 等)は改修規模を絞るため静かに無視する
438                // (マッチ条件には影響しない = 常にスキップ扱い)。
439                flush(mode, &mut cur, &mut c);
440                let mut j = i + 1;
441                let name_start = j;
442                while j < chars.len() && (chars[j].is_alphanumeric() || chars[j] == '-') {
443                    j += 1;
444                }
445                let name: String = chars[name_start..j].iter().collect::<String>().to_lowercase();
446                if j < chars.len() && chars[j] == '(' {
447                    let paren_start = j + 1;
448                    let mut depth = 1;
449                    let mut k = paren_start;
450                    while k < chars.len() && depth > 0 {
451                        match chars[k] {
452                            '(' => depth += 1,
453                            ')' => depth -= 1,
454                            _ => {}
455                        }
456                        if depth > 0 {
457                            k += 1;
458                        }
459                    }
460                    if name == "not" {
461                        let inner: String = chars[paren_start..k.min(chars.len())].iter().collect();
462                        for part in inner.split(',') {
463                            if let Some(inner_compound) = parse_compound(part.trim()) {
464                                c.not.push(inner_compound);
465                            }
466                        }
467                    } else if name == "nth-child" {
468                        let inner: String = chars[paren_start..k.min(chars.len())].iter().collect();
469                        if let Some(formula) = parse_nth_formula(&inner) {
470                            c.pos.push(PosPseudo::Nth(formula));
471                        }
472                    } else if name == "nth-of-type" {
473                        let inner: String = chars[paren_start..k.min(chars.len())].iter().collect();
474                        if let Some(formula) = parse_nth_formula(&inner) {
475                            c.pos_of_type.push(PosPseudo::Nth(formula));
476                        }
477                    } else if name == "nth-last-child" {
478                        let inner: String = chars[paren_start..k.min(chars.len())].iter().collect();
479                        if let Some(formula) = parse_nth_formula(&inner) {
480                            c.pos.push(PosPseudo::NthLast(formula));
481                        }
482                    } else if name == "nth-last-of-type" {
483                        let inner: String = chars[paren_start..k.min(chars.len())].iter().collect();
484                        if let Some(formula) = parse_nth_formula(&inner) {
485                            c.pos_of_type.push(PosPseudo::NthLast(formula));
486                        }
487                    } else if name == "is" || name == "where" {
488                        let inner: String = chars[paren_start..k.min(chars.len())].iter().collect();
489                        for part in inner.split(',') {
490                            if let Some(inner_compound) = parse_compound(part.trim()) {
491                                c.is_list.push(inner_compound);
492                            }
493                        }
494                    }
495                    j = k;
496                } else {
497                    match name.as_str() {
498                        "first-child" => c.pos.push(PosPseudo::First),
499                        "last-child" => c.pos.push(PosPseudo::Last),
500                        "only-child" => c.pos.push(PosPseudo::Only),
501                        "first-of-type" => c.pos_of_type.push(PosPseudo::First),
502                        "last-of-type" => c.pos_of_type.push(PosPseudo::Last),
503                        "only-of-type" => c.pos_of_type.push(PosPseudo::Only),
504                        // `:scope`(Selectors Level 4。丸ごと未対応だった)。
505                        "scope" => c.is_scope = true,
506                        _ => {}
507                    }
508                }
509                i = j;
510                mode = ' ';
511            }
512            _ => cur.push(ch),
513        }
514        i += 1;
515    }
516    flush(mode, &mut cur, &mut c);
517    if c.tag.is_none()
518        && c.id.is_none()
519        && c.classes.is_empty()
520        && c.attrs.is_empty()
521        && c.not.is_empty()
522        && c.is_list.is_empty()
523        && c.pos.is_empty()
524        && c.pos_of_type.is_empty()
525        && !c.is_scope
526    {
527        return None;
528    }
529    Some(c)
530}
531
532/// セレクタ文字列全体を、トップレベル(`[...]`/`(...)` の外側)のカンマだけで
533/// 分割する(`selector.split(',')` を単純に使うと `:is(.a, .b)` の中のカンマも
534/// セレクタグループの区切りと誤認識し、`:is(.a` / ` .b)` という壊れた2グループに
535/// 分解されてしまうバグがあった。`node_matches`/`query_all` の「カンマ区切りは
536/// OR」処理の入口はここを必ず経由すること)。
537fn split_top_level_commas(s: &str) -> Vec<String> {
538    let mut parts = Vec::new();
539    let mut cur = String::new();
540    let mut depth = 0i32;
541    for ch in s.chars() {
542        match ch {
543            '[' | '(' => {
544                depth += 1;
545                cur.push(ch);
546            }
547            ']' | ')' => {
548                depth -= 1;
549                cur.push(ch);
550            }
551            ',' if depth == 0 => {
552                parts.push(core::mem::take(&mut cur));
553            }
554            _ => cur.push(ch),
555        }
556    }
557    parts.push(cur);
558    parts
559}
560
561/// カンマで区切られた1グループ分のセレクタ文字列を、結合子記号(`>`/`+`/`~`)と
562/// 空白区切りのコンパウンドセレクタ文字列トークンへ分割する(`[...]`/`(...)` 内の
563/// 空白は区切りとして扱わない)。
564fn tokenize_selector_group(s: &str) -> Vec<String> {
565    let mut tokens = Vec::new();
566    let mut cur = String::new();
567    let mut depth = 0i32;
568    for ch in s.chars() {
569        match ch {
570            '[' | '(' => {
571                depth += 1;
572                cur.push(ch);
573            }
574            ']' | ')' => {
575                depth -= 1;
576                cur.push(ch);
577            }
578            '>' | '+' | '~' if depth == 0 => {
579                if !cur.trim().is_empty() {
580                    tokens.push(cur.trim().to_string());
581                    cur.clear();
582                }
583                tokens.push(ch.to_string());
584            }
585            c if c.is_whitespace() && depth == 0 => {
586                if !cur.trim().is_empty() {
587                    tokens.push(cur.trim().to_string());
588                    cur.clear();
589                }
590            }
591            _ => cur.push(ch),
592        }
593    }
594    if !cur.trim().is_empty() {
595        tokens.push(cur.trim().to_string());
596    }
597    tokens
598}
599
600/// トークン列(コンパウンド文字列と結合子記号が混在)から `SelStep` 列を組み立てる。
601fn build_steps(tokens: &[String]) -> Vec<SelStep> {
602    let mut steps: Vec<SelStep> = Vec::new();
603    let mut pending: Option<Combinator> = None;
604    for tok in tokens {
605        match tok.as_str() {
606            ">" => pending = Some(Combinator::Child),
607            "+" => pending = Some(Combinator::Adjacent),
608            "~" => pending = Some(Combinator::General),
609            _ => {
610                let combinator = if steps.is_empty() {
611                    None
612                } else {
613                    Some(pending.take().unwrap_or(Combinator::Descendant))
614                };
615                if let Some(compound) = parse_compound(tok) {
616                    steps.push(SelStep { compound, combinator });
617                }
618            }
619        }
620    }
621    steps
622}
623
624impl DomBridge {
625    pub fn new() -> Self {
626        DomBridge {
627            nodes: Vec::new(),
628            listeners: Vec::new(),
629            dirty: false,
630            built: false,
631            rects: BTreeMap::new(),
632            border_widths: BTreeMap::new(),
633            computed_styles: BTreeMap::new(),
634            scroll_tops: BTreeMap::new(),
635            scroll_heights: BTreeMap::new(),
636            next_listener_id: 1,
637            mut_observers: Vec::new(),
638            mut_records: Vec::new(),
639            next_mut_obs_id: 1,
640            pointer_captures: BTreeSet::new(),
641            focused_idx: None,
642            pending_scroll_abs_y: None,
643            pending_scroll_by_y: None,
644            on_handlers: BTreeMap::new(),
645        }
646    }
647
648    /// `element.onXXX = fn`(関数値)の永続的な書き込み。
649    pub fn set_on_handler(&mut self, node: usize, key: &str, val: Value) {
650        self.on_handlers.insert((node, key.to_string()), val);
651    }
652    /// `element.onXXX`の読み出し。未設定なら`None`。
653    pub fn get_on_handler(&self, node: usize, key: &str) -> Option<Value> {
654        self.on_handlers.get(&(node, key.to_string())).cloned()
655    }
656
657    /// `element.setPointerCapture(pointerId)`。
658    pub fn set_pointer_capture(&mut self, idx: usize, pointer_id: i32) {
659        self.pointer_captures.insert((idx, pointer_id));
660    }
661    /// `element.releasePointerCapture(pointerId)`。
662    pub fn release_pointer_capture(&mut self, idx: usize, pointer_id: i32) {
663        self.pointer_captures.remove(&(idx, pointer_id));
664    }
665    /// `element.hasPointerCapture(pointerId)`。
666    pub fn has_pointer_capture(&self, idx: usize, pointer_id: i32) -> bool {
667        self.pointer_captures.contains(&(idx, pointer_id))
668    }
669
670    /// MutationObserver: childList 変化を記録するヘルパ。
671    fn notify_child_list(&mut self, parent_idx: usize, added: Vec<usize>, removed: Vec<usize>) {
672        for obs in &self.mut_observers {
673            let watches = obs.child_list
674                && (obs.target_idx == parent_idx
675                    || (obs.subtree && self.is_ancestor_of(obs.target_idx, parent_idx)));
676            if watches {
677                self.mut_records.push((
678                    obs.id,
679                    MutRec {
680                        type_: "childList",
681                        target_idx: parent_idx,
682                        added: added.clone(),
683                        removed: removed.clone(),
684                        attribute_name: None,
685                        old_value: None,
686                    },
687                ));
688            }
689        }
690    }
691
692    /// MutationObserver: 属性変化を記録するヘルパ。`attributeFilter` が指定された
693    /// 観察者はその一覧に無い属性名を無視し、`attributeOldValue` が指定された
694    /// 観察者にのみ変更前の値をレコードへ積む(以前はどちらのオプションも
695    /// 読み取られておらず、`attributeFilter` 指定時も無関係な属性の変化まで
696    /// 全て通知され、`oldValue` は常に `null` 固定だった)。
697    fn notify_attribute(&mut self, idx: usize, attr_name: &str, old_value: Option<&str>) {
698        for obs in &self.mut_observers {
699            let watches = obs.attributes
700                && (obs.target_idx == idx
701                    || (obs.subtree && self.is_ancestor_of(obs.target_idx, idx)))
702                && obs
703                    .attribute_filter
704                    .as_ref()
705                    .is_none_or(|f| f.iter().any(|n| n == attr_name));
706            if watches {
707                self.mut_records.push((
708                    obs.id,
709                    MutRec {
710                        type_: "attributes",
711                        target_idx: idx,
712                        added: Vec::new(),
713                        removed: Vec::new(),
714                        attribute_name: Some(String::from(attr_name)),
715                        old_value: if obs.attribute_old_value {
716                            Some(old_value.unwrap_or("").to_string())
717                        } else {
718                            None
719                        },
720                    },
721                ));
722            }
723        }
724    }
725
726    /// MutationObserver: テキストノードの内容変化を記録するヘルパ(丸ごと
727    /// 未対応だった。`notify_attribute`と対になる`characterData`版。
728    /// `idx`は変化した**テキストノード自身**のインデックス〔仕様上
729    /// `CharacterData`のmutationはテキストノード自身がtargetになる〕。
730    /// 2026-07-17 発見・実装)。
731    fn notify_character_data(&mut self, idx: usize, old_value: Option<&str>) {
732        for obs in &self.mut_observers {
733            let watches = obs.character_data
734                && (obs.target_idx == idx
735                    || (obs.subtree && self.is_ancestor_of(obs.target_idx, idx)));
736            if watches {
737                self.mut_records.push((
738                    obs.id,
739                    MutRec {
740                        type_: "characterData",
741                        target_idx: idx,
742                        added: Vec::new(),
743                        removed: Vec::new(),
744                        attribute_name: None,
745                        old_value: if obs.character_data_old_value {
746                            Some(old_value.unwrap_or("").to_string())
747                        } else {
748                            None
749                        },
750                    },
751                ));
752            }
753        }
754    }
755
756    /// レンダラがレイアウト後に要素矩形を登録する(getBoundingClientRect 等が参照)。
757    pub fn set_rect(&mut self, idx: usize, x: i32, y: i32, w: i32, h: i32) {
758        self.rects.insert(idx, (x, y, w, h));
759    }
760    pub fn get_rect(&self, idx: usize) -> Option<(i32, i32, i32, i32)> {
761        self.rects.get(&idx).copied()
762    }
763    pub fn set_border_widths(&mut self, idx: usize, top: i32, right: i32, bottom: i32, left: i32) {
764        self.border_widths.insert(idx, (top, right, bottom, left));
765    }
766    pub fn get_border_widths(&self, idx: usize) -> (i32, i32, i32, i32) {
767        self.border_widths.get(&idx).copied().unwrap_or((0, 0, 0, 0))
768    }
769    pub fn clear_rects(&mut self) {
770        self.rects.clear();
771        self.border_widths.clear();
772    }
773
774    /// 算出スタイルを登録(kebab-case プロパティ→値)。
775    pub fn set_computed_style(&mut self, idx: usize, prop: &str, val: &str) {
776        self.computed_styles
777            .entry(idx)
778            .or_default()
779            .insert(String::from(prop), String::from(val));
780    }
781    /// 算出スタイルを取得(無ければインライン style → 空)。
782    pub fn get_computed(&self, idx: usize, prop: &str) -> String {
783        if let Some(m) = self.computed_styles.get(&idx) {
784            if let Some(v) = m.get(prop) {
785                return v.clone();
786            }
787        }
788        self.nodes
789            .get(idx)
790            .and_then(|n| n.style.get(prop))
791            .cloned()
792            .unwrap_or_default()
793    }
794    pub fn clear_computed(&mut self) {
795        self.computed_styles.clear();
796    }
797
798    /// 新しいページの DOM 木からブリッジを再構築する(リスナはクリア)。
799    pub fn build_from(&mut self, root: &Node) {
800        self.nodes.clear();
801        self.listeners.clear();
802        self.dirty = false;
803        self.built = true;
804        Self::collect(root, None, &mut self.nodes);
805        // 各要素の initial_text をドキュメント順走査で集約。
806        let n = self.nodes.len();
807        for i in 0..n {
808            if !self.nodes[i].is_text {
809                let t = self.collect_text(i);
810                self.nodes[i].initial_text = t;
811            }
812        }
813        // `<details name="...">`排他グループの初期状態正規化(HTML5。丸ごと
814        // 未対応だった)。クリック操作時の排他挙動は`render.rs`側で対応
815        // 済みだが、HTML記述時点で同じ`name`を持つ複数の`<details open>`が
816        // 並んでいた場合、仕様上は文書順で最後の1つだけが開いた状態のまま
817        // 残り、それより前のものは自動的に閉じられる必要がある。この
818        // パース直後の正規化が漏れていた。2026-07-18 発見・実装)。
819        {
820            let mut last_open_by_name: alloc::collections::BTreeMap<String, usize> = alloc::collections::BTreeMap::new();
821            for i in 0..n {
822                let node = &self.nodes[i];
823                if node.tag == "details" && node.attrs.contains_key("open") {
824                    if let Some(name) = node.attrs.get("name").filter(|s| !s.is_empty()) {
825                        last_open_by_name.insert(name.clone(), i);
826                    }
827                }
828            }
829            for i in 0..n {
830                let node = &self.nodes[i];
831                if node.tag == "details" && node.attrs.contains_key("open") {
832                    if let Some(name) = node.attrs.get("name").filter(|s| !s.is_empty()) {
833                        if last_open_by_name.get(name) != Some(&i) {
834                            self.nodes[i].attrs.remove("open");
835                        }
836                    }
837                }
838            }
839        }
840    }
841
842    /// 木をドキュメント順(プリオーダ)に再帰収集。子インデックスを記録する。
843    fn collect(node: &Node, parent: Option<usize>, out: &mut Vec<DomNode>) -> usize {
844        let idx = out.len();
845        let dn = match &node.node_type {
846            NodeType::Text(t) => DomNode {
847                tag: String::new(),
848                is_text: true,
849                id: String::new(),
850                classes: Vec::new(),
851                style: BTreeMap::new(),
852                attrs: BTreeMap::new(),
853                text_override: None,
854                initial_text: t.clone(),
855                parent,
856                children: Vec::new(),
857                classes_dirty: false,
858                style_dirty: false,
859            },
860            NodeType::Element {
861                tag_name,
862                attributes,
863                classes,
864                id,
865            } => {
866                let style =
867                    parse_inline_style(attributes.get("style").map(|s| s.as_str()).unwrap_or(""));
868                DomNode {
869                    tag: tag_name.clone(),
870                    is_text: false,
871                    id: id.clone().unwrap_or_default(),
872                    classes: classes.clone(),
873                    style,
874                    attrs: attributes.clone(),
875                    text_override: None,
876                    initial_text: String::new(),
877                    parent,
878                    children: Vec::new(),
879                    classes_dirty: false,
880                    style_dirty: false,
881                }
882            }
883        };
884        out.push(dn);
885        let mut kids = Vec::new();
886        for c in &node.children {
887            kids.push(Self::collect(c, Some(idx), out));
888        }
889        out[idx].children = kids;
890        idx
891    }
892
893    /// 部分木のテキストを連結(textContent 相当)。現在値(override 優先)で集約。
894    fn collect_text(&self, idx: usize) -> String {
895        let node = &self.nodes[idx];
896        if node.tag == "#comment" {
897            return node.text_override.clone().unwrap_or_else(|| node.initial_text.clone());
898        }
899        let mut s = String::new();
900        self.collect_text_into(idx, &mut s);
901        s
902    }
903    fn collect_text_into(&self, idx: usize, out: &mut String) {
904        let node = &self.nodes[idx];
905        if node.is_text {
906            out.push_str(node.text_override.as_deref().unwrap_or(&node.initial_text));
907            return;
908        }
909        for &c in &node.children {
910            self.collect_text_into(c, out);
911        }
912    }
913
914    // ---- 参照系 ----
915
916    /// id → ノード添字の対応表を 1 回で作る。
917    ///
918    /// 【2026-08-05】`get_element_by_id` は全ノードの線形走査で、
919    /// しかも各ノードで `is_attached` が親を辿る。
920    /// レイアウト後の矩形反映では**要素 1 個ごとに**これを呼んでおり、
921    /// O(要素 × ノード × 深さ) になっていた(実測で要素・ノードとも約 369)。
922    ///
923    /// 走査 1 回で表を作れば、引く側は O(log N) で済む。
924    /// 条件(テキストノードでない・id が空でない・ルートから到達可能)は
925    /// `get_element_by_id` と揃えてある。同じ id が複数あれば先勝ち
926    /// (`find` と同じ)。
927    ///
928    /// 計算量: **O(N × D)**(N はノード数、D は木の深さ)。
929    pub fn build_id_index(&self) -> alloc::collections::BTreeMap<&str, usize> {
930        let mut map: alloc::collections::BTreeMap<&str, usize> =
931            alloc::collections::BTreeMap::new();
932        if !self.built {
933            return map;
934        }
935        for i in 0..self.nodes.len() {
936            let n = &self.nodes[i];
937            if n.is_text || n.id.is_empty() {
938                continue;
939            }
940            if !self.is_attached(i) {
941                continue;
942            }
943            map.entry(n.id.as_str()).or_insert(i);
944        }
945        map
946    }
947
948    pub fn get_element_by_id(&self, id: &str) -> Option<usize> {
949        if !self.built {
950            return None;
951        }
952        if let Some(rest) = id.strip_prefix("_node_idx_") {
953            if let Ok(idx) = rest.parse::<usize>() {
954                if idx < self.nodes.len() && !self.nodes[idx].is_text && self.is_attached(idx) {
955                    return Some(idx);
956                }
957            }
958        }
959        (0..self.nodes.len())
960            .find(|&i| !self.nodes[i].is_text && self.nodes[i].id == id && self.is_attached(i))
961    }
962
963    /// node[0](ルート)から到達可能か。createElement 直後や remove 済みは false。
964    /// `node.isConnected`(丸ごと未対応だった)向けに公開する。
965    pub fn is_attached(&self, idx: usize) -> bool {
966        let mut cur = idx;
967        let mut guard = 0;
968        loop {
969            if cur == 0 {
970                return true;
971            }
972            match self.nodes.get(cur).and_then(|n| n.parent) {
973                Some(p) => {
974                    cur = p;
975                    guard += 1;
976                    if guard > self.nodes.len() {
977                        return false;
978                    }
979                }
980                None => return false,
981            }
982        }
983    }
984
985    /// 単純セレクタ(`#id` / `.class` / `tag`、先頭一致)にマッチする最初のノード。
986    pub fn query(&self, selector: &str) -> Option<usize> {
987        self.query_all(selector).into_iter().next()
988    }
989
990    /// セレクタにマッチする全ノード(ドキュメント順)。以前は `node_matches` と同じ
991    /// 単純セレクタ限定の簡略実装が独立に重複しており、`querySelectorAll('div.active')`
992    /// 等のコンパウンドセレクタが常に空になる同型のバグだった。`node_matches` と
993    /// 共通の `compound_matches`/`match_selector_chain` に統一して解消する。
994    pub fn query_all(&self, selector: &str) -> Vec<usize> {
995        self.query_all_scoped(selector, None)
996    }
997
998    /// `query_all` の `:scope` 対応版。`element.querySelectorAll`/`querySelector`
999    /// の起点要素を `scope_root` として渡すと、セレクタ中の `:scope` 疑似クラス
1000    /// (Selectors Level 4。丸ごと未対応だった)がそのノードにのみ一致するように
1001    /// なる(詳細は `node_matches_scoped` 参照)。
1002    pub fn query_all_scoped(&self, selector: &str, scope_root: Option<usize>) -> Vec<usize> {
1003        let sel = selector.trim();
1004        let mut out = Vec::new();
1005        if !self.built || sel.is_empty() {
1006            return out;
1007        }
1008        for i in 0..self.nodes.len() {
1009            if !self.is_attached(i) {
1010                continue;
1011            }
1012            if self.node_matches_scoped(i, sel, scope_root) {
1013                out.push(i);
1014            }
1015        }
1016        out
1017    }
1018
1019    // ---- 変更系 ----
1020
1021    /// 要素の textContent を設定。子孫テキストノードがあれば最初に値を入れ残りを空に、
1022    /// 無ければ既存の子を捨てて単一テキスト子を作る(createElement したノード等)。
1023    pub fn set_text_content(&mut self, idx: usize, val: &str) {
1024        if idx >= self.nodes.len() {
1025            return;
1026        }
1027        if self.nodes[idx].tag == "#comment" {
1028            let old_val = self.nodes[idx]
1029                .text_override
1030                .as_deref()
1031                .unwrap_or(&self.nodes[idx].initial_text)
1032                .to_string();
1033            self.nodes[idx].text_override = Some(val.to_string());
1034            if old_val != val {
1035                self.notify_character_data(idx, Some(&old_val));
1036            }
1037            self.dirty = true;
1038            return;
1039        }
1040        let text_nodes = self.descendant_text_nodes(idx);
1041        if text_nodes.is_empty() && !self.nodes[idx].is_text {
1042            // テキスト子を新設(子が1件も無かった場合のみ通る経路。この childList
1043            // 変化も他の子ノード追加経路と同様に MutationObserver へ通知する)。
1044            let t = self.new_text_node(val);
1045            self.nodes[idx].children.clear();
1046            self.nodes[t].parent = Some(idx);
1047            self.nodes[idx].children.push(t);
1048            self.notify_child_list(idx, alloc::vec![t], Vec::new());
1049        } else {
1050            for (k, t) in text_nodes.iter().enumerate() {
1051                let new_val = if k == 0 { val.to_string() } else { String::new() };
1052                // `characterData`監視(丸ごと未対応だった。テキストノードの
1053                // 内容が実際に変わった場合のみ通知する。詳細は
1054                // `notify_character_data`のdocコメント参照。2026-07-17
1055                // 発見・実装)。
1056                let old_val = self.nodes[*t]
1057                    .text_override
1058                    .as_deref()
1059                    .unwrap_or(&self.nodes[*t].initial_text)
1060                    .to_string();
1061                self.nodes[*t].text_override = Some(new_val.clone());
1062                if old_val != new_val {
1063                    self.notify_character_data(*t, Some(&old_val));
1064                }
1065            }
1066        }
1067        self.dirty = true;
1068    }
1069
1070    /// `node.normalize()`(丸ごと未対応だった。DOM 標準メソッドで、連続する隣接テキスト
1071    /// ノードを1つに連結し、空のテキストノードを取り除く。`textContent` を何度も
1072    /// 部分的に書き換えた後などに呼ばれる定番の掃除用メソッド)。子孫を再帰的に処理する。
1073    pub fn normalize_node(&mut self, idx: usize) {
1074        if idx >= self.nodes.len() || self.nodes[idx].is_text {
1075            return;
1076        }
1077        let children = self.nodes[idx].children.clone();
1078        let mut run_start: Option<usize> = None;
1079        let mut to_remove: Vec<usize> = Vec::new();
1080        for &c in &children {
1081            if self.nodes[c].is_text {
1082                match run_start {
1083                    None => run_start = Some(c),
1084                    Some(first) => {
1085                        let more = self.text_of(c);
1086                        let mut cur = self.text_of(first);
1087                        cur.push_str(&more);
1088                        self.nodes[first].text_override = Some(cur);
1089                        to_remove.push(c);
1090                    }
1091                }
1092            } else {
1093                run_start = None;
1094                self.normalize_node(c);
1095            }
1096        }
1097        for &c in &children {
1098            if self.nodes.get(c).map(|n| n.is_text).unwrap_or(false)
1099                && !to_remove.contains(&c)
1100                && self.text_of(c).is_empty()
1101            {
1102                to_remove.push(c);
1103            }
1104        }
1105        for r in to_remove {
1106            self.remove_node(r);
1107        }
1108    }
1109    /// テキストノードの実効テキスト(`text_override` があればそれ、無ければ
1110    /// `initial_text`)。`collect_text_into` と同じ参照パターン。
1111    fn text_of(&self, idx: usize) -> String {
1112        let n = &self.nodes[idx];
1113        n.text_override.as_deref().unwrap_or(&n.initial_text).to_string()
1114    }
1115
1116    // ---- 構造変更(createElement / appendChild / removeChild / innerHTML) ----
1117
1118    fn new_element_node(&mut self, tag: &str) -> usize {
1119        let idx = self.nodes.len();
1120        self.nodes.push(DomNode {
1121            tag: tag.to_lowercase(),
1122            is_text: false,
1123            id: String::new(),
1124            classes: Vec::new(),
1125            style: BTreeMap::new(),
1126            attrs: BTreeMap::new(),
1127            text_override: None,
1128            initial_text: String::new(),
1129            parent: None,
1130            children: Vec::new(),
1131            classes_dirty: false,
1132            style_dirty: false,
1133        });
1134        idx
1135    }
1136
1137    fn new_text_node(&mut self, text: &str) -> usize {
1138        let idx = self.nodes.len();
1139        self.nodes.push(DomNode {
1140            tag: String::new(),
1141            is_text: true,
1142            id: String::new(),
1143            classes: Vec::new(),
1144            style: BTreeMap::new(),
1145            attrs: BTreeMap::new(),
1146            text_override: Some(text.to_string()),
1147            initial_text: text.to_string(),
1148            parent: None,
1149            children: Vec::new(),
1150            classes_dirty: false,
1151            style_dirty: false,
1152        });
1153        idx
1154    }
1155
1156    /// `document.createElement(tag)`。デタッチ状態のノードを返す。
1157    pub fn create_element(&mut self, tag: &str) -> usize {
1158        self.new_element_node(tag)
1159    }
1160
1161    /// `document.createTextNode(text)`。
1162    pub fn create_text_node(&mut self, text: &str) -> usize {
1163        self.new_text_node(text)
1164    }
1165
1166    /// `document.createComment(text)`。
1167    pub fn create_comment(&mut self, text: &str) -> usize {
1168        let idx = self.nodes.len();
1169        self.nodes.push(DomNode {
1170            tag: String::from("#comment"),
1171            is_text: false,
1172            id: String::new(),
1173            classes: Vec::new(),
1174            style: BTreeMap::new(),
1175            attrs: BTreeMap::new(),
1176            text_override: Some(text.to_string()),
1177            initial_text: text.to_string(),
1178            parent: None,
1179            children: Vec::new(),
1180            classes_dirty: false,
1181            style_dirty: false,
1182        });
1183        idx
1184    }
1185
1186    /// `document.createDocumentFragment()`(丸ごと未対応だった)。仕様どおり、
1187    /// `#fragment` という特殊タグの要素として表現する。この要素自身は決して
1188    /// `root` から到達可能(`is_attached`)にならず、`append_child`/`insert_before`
1189    /// に渡されると自身ではなく子要素群がそのまま挿入先へ移動し、自身は空になる
1190    /// (実 DOM の DocumentFragment の「挿入すると中身だけが移動する」挙動を再現)。
1191    pub fn create_document_fragment(&mut self) -> usize {
1192        self.new_element_node("#fragment")
1193    }
1194
1195    /// `template.content`(`<template>` が丸ごと未対応だった)。仕様上 `<template>`
1196    /// の子は「不活性」で通常の DOM ツリーには存在せず、代わりに `.content`
1197    /// (`DocumentFragment`)経由でのみアクセスできる。この処理系は HTML パース時に
1198    /// `template` を特別扱いしないため子が普通にライブツリーへ入ってしまうが、
1199    /// 初回アクセス時にその子を `#fragment` ノードへ移し替え(内部専用属性
1200    /// `_content_frag` にフラグメントの index をキャッシュ)、以後は同じフラグメントを
1201    /// 返すことで「以後 `<template>` 自身は空、`.content` だけが中身を持つ」という
1202    /// 仕様どおりの見た目に近づける。
1203    pub fn template_content(&mut self, idx: usize) -> usize {
1204        if let Some(f) = self
1205            .get_attr(idx, "_content_frag")
1206            .and_then(|s| s.parse::<usize>().ok())
1207        {
1208            return f;
1209        }
1210        let f = self.create_document_fragment();
1211        let kids = core::mem::take(&mut self.nodes[idx].children);
1212        for &k in &kids {
1213            self.nodes[k].parent = Some(f);
1214        }
1215        self.nodes[f].children = kids;
1216        self.set_attr(idx, "_content_frag", &f.to_string());
1217        f
1218    }
1219
1220    /// `child` が DocumentFragment(`#fragment` タグ)なら、その子要素群を
1221    /// 親から切り離して返し、フラグメント自身は空にする。それ以外は `[child]` を
1222    /// そのまま返す(`append_child`/`insert_before` の共通前処理)。
1223    fn expand_fragment(&mut self, child: usize) -> Vec<usize> {
1224        if self.nodes.get(child).map(|n| n.tag == "#fragment" || n.tag == "#document-fragment").unwrap_or(false) {
1225            let kids = core::mem::take(&mut self.nodes[child].children);
1226            for &k in &kids {
1227                self.nodes[k].parent = None;
1228            }
1229            kids
1230        } else {
1231            alloc::vec![child]
1232        }
1233    }
1234
1235    /// `parent.appendChild(child)`。既存の親から外して付け替える。`child` が
1236    /// DocumentFragment の場合は仕様どおりその子要素群を代わりに追加する。
1237    pub fn append_child(&mut self, parent: usize, child: usize) {
1238        if parent >= self.nodes.len() || child >= self.nodes.len() || parent == child {
1239            return;
1240        }
1241        let kids = self.expand_fragment(child);
1242        if kids.is_empty() {
1243            return;
1244        }
1245        for &k in &kids {
1246            self.detach(k);
1247            self.nodes[k].parent = Some(parent);
1248            self.nodes[parent].children.push(k);
1249        }
1250        self.dirty = true;
1251        self.notify_child_list(parent, kids, Vec::new());
1252    }
1253
1254    /// `parent.removeChild(child)` / `child.remove()`。
1255    pub fn remove_child(&mut self, parent: usize, child: usize) {
1256        let p = if parent < self.nodes.len() { Some(parent) } else { self.nodes.get(child).and_then(|n| n.parent) };
1257        self.detach(child);
1258        self.dirty = true;
1259        if let Some(p) = p { self.notify_child_list(p, Vec::new(), alloc::vec![child]); }
1260    }
1261    pub fn remove_node(&mut self, idx: usize) {
1262        let p = self.nodes.get(idx).and_then(|n| n.parent);
1263        self.detach(idx);
1264        self.dirty = true;
1265        if let Some(p) = p { self.notify_child_list(p, Vec::new(), alloc::vec![idx]); }
1266    }
1267
1268    /// `parent.insertBefore(new_child, ref_child)`。ref が None/不在なら末尾に追加。
1269    /// `new_child` が DocumentFragment の場合は仕様どおりその子要素群を代わりに挿入する。
1270    pub fn insert_before(&mut self, parent: usize, new_child: usize, ref_child: Option<usize>) {
1271        if parent >= self.nodes.len() || new_child >= self.nodes.len() || parent == new_child {
1272            return;
1273        }
1274        let kids = self.expand_fragment(new_child);
1275        if kids.is_empty() {
1276            return;
1277        }
1278        let pos = ref_child.and_then(|r| self.nodes[parent].children.iter().position(|&c| c == r));
1279        for (i, &k) in kids.iter().enumerate() {
1280            self.detach(k);
1281            self.nodes[k].parent = Some(parent);
1282            match pos {
1283                Some(p) => self.nodes[parent].children.insert(p + i, k),
1284                None => self.nodes[parent].children.push(k),
1285            }
1286        }
1287        self.dirty = true;
1288        self.notify_child_list(parent, kids, Vec::new());
1289    }
1290
1291    /// `parent.replaceChild(new_child, old_child)`。old を new で置換。
1292    pub fn replace_child(&mut self, parent: usize, new_child: usize, old_child: usize) {
1293        if parent >= self.nodes.len() || new_child >= self.nodes.len() {
1294            return;
1295        }
1296        if !self.nodes[parent].children.contains(&old_child) {
1297            return;
1298        }
1299        self.detach(new_child); // どこかに属していれば外す(位置が変わりうる)
1300        if let Some(p) = self.nodes[parent]
1301            .children
1302            .iter()
1303            .position(|&c| c == old_child)
1304        {
1305            self.nodes[old_child].parent = None;
1306            self.nodes[parent].children[p] = new_child;
1307            self.nodes[new_child].parent = Some(parent);
1308            self.dirty = true;
1309            self.notify_child_list(parent, alloc::vec![new_child], alloc::vec![old_child]);
1310        }
1311    }
1312
1313    /// `node.cloneNode(deep)`。属性/テキストをコピーした新ノードを作る(親なし)。deep なら子も再帰。
1314    pub fn clone_node(&mut self, idx: usize, deep: bool) -> usize {
1315        if idx >= self.nodes.len() {
1316            return idx;
1317        }
1318        let (is_text, tag, id, classes, style, attrs, text_override, initial_text, children) = {
1319            let n = &self.nodes[idx];
1320            (
1321                n.is_text,
1322                n.tag.clone(),
1323                n.id.clone(),
1324                n.classes.clone(),
1325                n.style.clone(),
1326                n.attrs.clone(),
1327                n.text_override.clone(),
1328                n.initial_text.clone(),
1329                n.children.clone(),
1330            )
1331        };
1332        let new_idx = self.nodes.len();
1333        self.nodes.push(DomNode {
1334            tag,
1335            is_text,
1336            id,
1337            classes,
1338            style,
1339            attrs,
1340            text_override,
1341            initial_text,
1342            parent: None,
1343            children: Vec::new(),
1344            classes_dirty: false,
1345            style_dirty: false,
1346        });
1347        if deep && !is_text {
1348            for c in children {
1349                let cc = self.clone_node(c, true);
1350                self.nodes[cc].parent = Some(new_idx);
1351                self.nodes[new_idx].children.push(cc);
1352            }
1353        }
1354        // `<template>.content` は通常の `children` ではなく `_content_frag` 属性
1355        // 経由で参照される遅延切り出し済み `#fragment` のため、上の通常の
1356        // children クローンでは複製されない。属性は他の属性と同様に文字列と
1357        // してそのままコピーされるので、対処しないとクローン先の
1358        // `template.content` が元の(同じ)フラグメントを指したまま共有されて
1359        // しまうバグだった(`.content` に一度でもアクセス済みの `<template>` を
1360        // `cloneNode()` した場合にのみ顕在化する)。仕様上 `<template>` の
1361        // content クローンは `deep` フラグに関わらず常に行われる特殊規則のため、
1362        // `if deep` の外で無条件に複製する。
1363        let frag_attr = self.nodes[new_idx]
1364            .attrs
1365            .get("_template_content_idx")
1366            .or_else(|| self.nodes[new_idx].attrs.get("_content_frag"))
1367            .cloned();
1368        if let Some(frag_idx) = frag_attr.and_then(|s| s.parse::<usize>().ok()) {
1369            let new_frag = self.clone_node(frag_idx, true);
1370            self.nodes[new_idx]
1371                .attrs
1372                .insert("_template_content_idx".to_string(), new_frag.to_string());
1373        }
1374        self.dirty = true;
1375        new_idx
1376    }
1377
1378    /// 親の children から idx を外す。
1379    fn detach(&mut self, idx: usize) {
1380        if idx >= self.nodes.len() {
1381            return;
1382        }
1383        if let Some(p) = self.nodes[idx].parent {
1384            if let Some(pos) = self.nodes[p].children.iter().position(|&c| c == idx) {
1385                self.nodes[p].children.remove(pos);
1386            }
1387        }
1388        self.nodes[idx].parent = None;
1389    }
1390
1391    /// 単一ノードを基準ノード idx の相対位置へ挿入(insertAdjacent* 共通)。
1392    pub fn insert_adjacent_node(&mut self, idx: usize, position: &str, node: usize) {
1393        match position {
1394            "beforeend" => self.append_child(idx, node),
1395            "afterbegin" => {
1396                let first = self
1397                    .nodes
1398                    .get(idx)
1399                    .and_then(|n| n.children.first().copied());
1400                self.insert_before(idx, node, first);
1401            }
1402            "beforebegin" => {
1403                if let Some(p) = self.nodes.get(idx).and_then(|n| n.parent) {
1404                    self.insert_before(p, node, Some(idx));
1405                }
1406            }
1407            "afterend" => {
1408                if let Some(p) = self.nodes.get(idx).and_then(|n| n.parent) {
1409                    let next = self.nodes.get(p).and_then(|n| {
1410                        n.children
1411                            .iter()
1412                            .position(|&c| c == idx)
1413                            .and_then(|i| n.children.get(i + 1).copied())
1414                    });
1415                    self.insert_before(p, node, next);
1416                }
1417            }
1418            _ => {}
1419        }
1420    }
1421
1422    /// `element.insertAdjacentHTML(position, html)`。html をパースして相対位置へ挿入。
1423    pub fn insert_adjacent_html(&mut self, idx: usize, position: &str, html: &str) {
1424        let root = crate::os_lib::dom::parse_html(html);
1425        let imported: Vec<usize> = root.children.iter().map(|c| self.import_node(c)).collect();
1426        if position == "afterbegin" {
1427            // 基準(元の先頭子)を一度だけ捕捉して順序保持で前挿入。
1428            let first = self
1429                .nodes
1430                .get(idx)
1431                .and_then(|n| n.children.first().copied());
1432            for node in imported {
1433                self.insert_before(idx, node, first);
1434            }
1435        } else {
1436            for node in imported {
1437                self.insert_adjacent_node(idx, position, node);
1438            }
1439        }
1440        self.dirty = true;
1441    }
1442
1443    /// `element.innerHTML = html`。フラグメントをパースして子を置換する。
1444    /// `appendChild`/`removeChild`/`insertBefore`/`replaceChild` は全て
1445    /// `notify_child_list` 経由で MutationObserver に通知していたが、
1446    /// `innerHTML` 代入だけがこの呼び出しを欠いており、最も頻繁に使われる
1447    /// DOM 書き換え手段にもかかわらず childList 変化が一切観察できなかった。
1448    pub fn set_inner_html(&mut self, idx: usize, html: &str) {
1449        if idx >= self.nodes.len() || self.nodes[idx].is_text {
1450            return;
1451        }
1452        let root = crate::os_lib::dom::parse_html(html);
1453        // 既存の子を切り離す。
1454        let old: Vec<usize> = self.nodes[idx].children.clone();
1455        for &c in &old {
1456            self.nodes[c].parent = None;
1457        }
1458        self.nodes[idx].children.clear();
1459        // パース結果(合成ルートの子)を import して付ける。
1460        let mut new_children = Vec::new();
1461        for child in &root.children {
1462            let ci = self.import_node(child);
1463            self.nodes[ci].parent = Some(idx);
1464            new_children.push(ci);
1465        }
1466        self.nodes[idx].children = new_children.clone();
1467        self.dirty = true;
1468        self.notify_child_list(idx, new_children, old);
1469    }
1470
1471    /// `element.outerHTML = html`。自ノードを HTML パース結果のノード群で置き換える。
1472    pub fn set_outer_html(&mut self, idx: usize, html: &str) {
1473        if idx >= self.nodes.len() {
1474            return;
1475        }
1476        let parent = match self.nodes[idx].parent {
1477            Some(p) => p,
1478            None => return,
1479        };
1480        let pos = match self.nodes[parent].children.iter().position(|&c| c == idx) {
1481            Some(p) => p,
1482            None => return,
1483        };
1484        let root = crate::os_lib::dom::parse_html(html);
1485        let mut new_inserted = Vec::new();
1486        for child in &root.children {
1487            let ci = self.import_node(child);
1488            self.nodes[ci].parent = Some(parent);
1489            new_inserted.push(ci);
1490        }
1491        self.nodes[idx].parent = None;
1492        self.nodes[parent].children.remove(pos);
1493        for (i, &ci) in new_inserted.iter().enumerate() {
1494            self.nodes[parent].children.insert(pos + i, ci);
1495        }
1496        self.dirty = true;
1497        self.notify_child_list(parent, new_inserted, alloc::vec![idx]);
1498    }
1499
1500    /// `dom::Node` 部分木をブリッジへ取り込み、ルート idx を返す。
1501    fn import_node(&mut self, node: &Node) -> usize {
1502        match &node.node_type {
1503            NodeType::Text(t) => self.new_text_node(t),
1504            NodeType::Element {
1505                tag_name,
1506                attributes,
1507                classes,
1508                id,
1509            } => {
1510                let idx = self.new_element_node(tag_name);
1511                {
1512                    let style = parse_inline_style(
1513                        attributes.get("style").map(|s| s.as_str()).unwrap_or(""),
1514                    );
1515                    let n = &mut self.nodes[idx];
1516                    n.id = id.clone().unwrap_or_default();
1517                    n.classes = classes.clone();
1518                    n.style = style;
1519                    n.attrs = attributes.clone();
1520                }
1521                let mut kids = Vec::new();
1522                for c in &node.children {
1523                    let ci = self.import_node(c);
1524                    self.nodes[ci].parent = Some(idx);
1525                    kids.push(ci);
1526                }
1527                self.nodes[idx].children = kids;
1528                idx
1529            }
1530        }
1531    }
1532
1533    /// ブリッジツリー(node[0] をルート)を `dom::Node` へ変換する。
1534    /// JS の変更(textContent/style/class/動的ノード)が反映された描画用ツリー。
1535    pub fn to_dom_node(&self) -> Node {
1536        if self.nodes.is_empty() {
1537            return Node::new_text(String::new());
1538        }
1539        self.build_node(0)
1540    }
1541
1542    fn build_node(&self, idx: usize) -> Node {
1543        let n = &self.nodes[idx];
1544        if n.is_text {
1545            return Node::new_text(
1546                n.text_override
1547                    .clone()
1548                    .unwrap_or_else(|| n.initial_text.clone()),
1549            );
1550        }
1551        let mut attrs = n.attrs.clone();
1552        let id = if n.id.is_empty() {
1553            let gen_id = alloc::format!("_node_idx_{}", idx);
1554            attrs.insert("id".to_string(), gen_id.clone());
1555            Some(gen_id)
1556        } else {
1557            attrs.insert("id".to_string(), n.id.clone());
1558            Some(n.id.clone())
1559        };
1560        if n.classes.is_empty() {
1561            attrs.remove("class");
1562        } else {
1563            attrs.insert("class".to_string(), n.classes.join(" "));
1564        }
1565        if n.style.is_empty() {
1566            attrs.remove("style");
1567        } else {
1568            attrs.insert("style".to_string(), serialize_inline_style(&n.style));
1569        }
1570        let mut node = Node::new_element_direct(n.tag.clone(), attrs, id, n.classes.clone());
1571        for &c in &n.children {
1572            node.children.push(self.build_node(c));
1573        }
1574        node
1575    }
1576
1577    /// 要素の textContent(現在値)を取得。
1578    pub fn get_text_content(&self, idx: usize) -> String {
1579        if idx >= self.nodes.len() {
1580            return String::new();
1581        }
1582        self.collect_text(idx)
1583    }
1584
1585    fn descendant_text_nodes(&self, idx: usize) -> Vec<usize> {
1586        let mut out = Vec::new();
1587        self.descendant_text_into(idx, &mut out);
1588        out
1589    }
1590    fn descendant_text_into(&self, idx: usize, out: &mut Vec<usize>) {
1591        let node = &self.nodes[idx];
1592        if node.is_text {
1593            out.push(idx);
1594            return;
1595        }
1596        for &c in &node.children {
1597            self.descendant_text_into(c, out);
1598        }
1599    }
1600
1601    // `style.xxx = ...`/`style.cssText = ...` は `class`/他の属性と同じく
1602    // `MutationObserver`(`attributes:true` + `attributeFilter:['style']` 等)が
1603    // 観察できるべき「style」属性の変化にもかかわらず、`class_add`/`class_remove`
1604    // と同型で `notify_attribute` を一切呼んでいなかった非対称バグ。
1605    pub fn set_style(&mut self, idx: usize, prop: &str, val: &str) {
1606        let old_value = {
1607            let Some(n) = self.nodes.get_mut(idx) else {
1608                return;
1609            };
1610            let old_value = serialize_inline_style(&n.style);
1611            let key = css_prop_from_camel(prop);
1612            if val.is_empty() {
1613                n.style.remove(&key);
1614            } else {
1615                n.style.insert(key, val.to_string());
1616            }
1617            n.style_dirty = true;
1618            old_value
1619        };
1620        self.dirty = true;
1621        self.notify_attribute(idx, "style", Some(&old_value));
1622    }
1623
1624    pub fn get_style(&self, idx: usize, prop: &str) -> String {
1625        let raw = self
1626            .nodes
1627            .get(idx)
1628            .and_then(|n| n.style.get(&css_prop_from_camel(prop)).cloned())
1629            .unwrap_or_default();
1630        strip_style_priority(&raw).0.to_string()
1631    }
1632
1633    /// `element.style.getPropertyPriority(name)`(丸ごと未対応だった。
1634    /// 2026-07-16 発見)。`!important` は生の値文字列末尾にそのまま保持
1635    /// されている(`set_style`/`css_prop_from_camel` 参照)ため、
1636    /// `strip_style_priority` で判定するだけで済む。
1637    pub fn get_style_priority(&self, idx: usize, prop: &str) -> String {
1638        let raw = self
1639            .nodes
1640            .get(idx)
1641            .and_then(|n| n.style.get(&css_prop_from_camel(prop)).cloned())
1642            .unwrap_or_default();
1643        if strip_style_priority(&raw).1 {
1644            "important".to_string()
1645        } else {
1646            String::new()
1647        }
1648    }
1649
1650    /// `element.style.cssText` の読み出し。以前は丸ごと未対応で、`cssText` を単なる
1651    /// 通常の CSS プロパティ名として扱う既存の汎用経路(`get_style`)に落ちてしまい、
1652    /// 常に空文字列を返していた(`serialize_inline_style` 自体は属性文字列の再構築で
1653    /// 既に使われていたが、JS からの読み出しには配線されていなかった)。
1654    pub fn get_css_text(&self, idx: usize) -> String {
1655        self.nodes
1656            .get(idx)
1657            .map(|n| serialize_inline_style(&n.style))
1658            .unwrap_or_default()
1659    }
1660    /// `element.style.cssText = "..."` の書き込み。単一プロパティとして扱われ常に
1661    /// 無視されていたのを、既存のスタイル属性パーサ(`parse_inline_style`)で丸ごと
1662    /// 置き換えるよう実装する。
1663    pub fn set_css_text(&mut self, idx: usize, text: &str) {
1664        let old_value = {
1665            let Some(n) = self.nodes.get_mut(idx) else {
1666                return;
1667            };
1668            let old_value = serialize_inline_style(&n.style);
1669            n.style = parse_inline_style(text);
1670            n.style_dirty = true;
1671            old_value
1672        };
1673        self.dirty = true;
1674        self.notify_attribute(idx, "style", Some(&old_value));
1675    }
1676
1677    /// `element.outerHTML`(読み出し専用。書き込み側は要素の親子関係の組み替えが
1678    /// 必要で改修規模が大きいため今回は見送り)が丸ごと未対応だった。自身のタグ・属性
1679    /// (id/class/style を含む)・子孫を再帰的に HTML 文字列へ直列化する。
1680    pub fn get_outer_html(&self, idx: usize) -> String {
1681        let mut out = String::new();
1682        self.serialize_node_html(idx, &mut out);
1683        out
1684    }
1685    /// `element.innerHTML` の読み出しが、実際にはマークアップ(子要素タグ)を含めず
1686    /// `textContent` と同じ「全テキストを連結しただけ」の値を返してしまっていたバグ。
1687    /// `<div>a<b>bold</b></div>` の `innerHTML` が仕様どおりの `"a<b>bold</b>"` ではなく
1688    /// `"abold"` になっていた。自身のタグは含めず、子ノードだけを直列化する。
1689    pub fn get_inner_html(&self, idx: usize) -> String {
1690        let mut out = String::new();
1691        if let Some(n) = self.nodes.get(idx) {
1692            for &c in &n.children {
1693                self.serialize_node_html(c, &mut out);
1694            }
1695        }
1696        out
1697    }
1698    fn serialize_node_html(&self, idx: usize, out: &mut String) {
1699        let Some(n) = self.nodes.get(idx) else { return };
1700        if n.is_text {
1701            out.push_str(&html_escape_text(
1702                n.text_override.as_deref().unwrap_or(&n.initial_text),
1703            ));
1704            return;
1705        }
1706        if n.tag == "#comment" {
1707            out.push_str("<!--");
1708            out.push_str(n.text_override.as_deref().unwrap_or(&n.initial_text));
1709            out.push_str("-->");
1710            return;
1711        }
1712        out.push('<');
1713        out.push_str(&n.tag);
1714        if !n.id.is_empty() {
1715            out.push_str(&format!(" id=\"{}\"", html_escape_attr(&n.id)));
1716        }
1717        if !n.classes.is_empty() {
1718            out.push_str(&format!(" class=\"{}\"", html_escape_attr(&n.classes.join(" "))));
1719        }
1720        if !n.style.is_empty() {
1721            out.push_str(&format!(
1722                " style=\"{}\"",
1723                html_escape_attr(&serialize_inline_style(&n.style))
1724            ));
1725        }
1726        // `n.attrs` には HTML パース時の生の `id`/`class` もそのまま残っている場合が
1727        // あり(`n.id`/`n.classes` は別途専用フィールドとして持つ)、除外しないと
1728        // `id="t" id="t"` のように重複出力してしまう。`_` 始まりの内部専用属性
1729        // (`_content_frag`/`_return_value`/`_custom_validity` 等、実装詳細を
1730        // `set_attr`/`get_attr` 経由で持ち回るための housekeeping 用の値)も
1731        // 除外しないと `outerHTML`/`innerHTML` にそのまま漏れて見えてしまう
1732        // バグだった。
1733        for (k, v) in &n.attrs {
1734            if k == "id" || k == "class" || k == "style" || k.starts_with('_') {
1735                continue;
1736            }
1737            out.push(' ');
1738            out.push_str(k);
1739            out.push_str("=\"");
1740            out.push_str(&html_escape_attr(v));
1741            out.push('"');
1742        }
1743        out.push('>');
1744        // void 要素(自身終了タグを持たない)。
1745        if matches!(
1746            n.tag.as_str(),
1747            "br" | "img" | "input" | "hr" | "meta" | "link" | "area" | "base" | "col"
1748                | "embed" | "param" | "source" | "track" | "wbr"
1749        ) {
1750            return;
1751        }
1752        for &c in &n.children {
1753            self.serialize_node_html(c, out);
1754        }
1755        out.push_str("</");
1756        out.push_str(&n.tag);
1757        out.push('>');
1758    }
1759
1760    // `classList.add`/`.remove` は `class` 属性を書き換えるにもかかわらず、
1761    // `setAttribute('class', ...)`/`removeAttribute()` と違って `notify_attribute`
1762    // を一切呼んでおらず、`MutationObserver` に「class」の変化が通知されない
1763    // 非対称バグだった(`set_attr`/`remove_attr` は既に通知していた)。
1764    pub fn class_add(&mut self, idx: usize, cls: &str) {
1765        let old_value = {
1766            let Some(n) = self.nodes.get_mut(idx) else {
1767                return;
1768            };
1769            if n.classes.iter().any(|c| c == cls) {
1770                return;
1771            }
1772            let old_value = n.classes.join(" ");
1773            n.classes.push(cls.to_string());
1774            n.classes_dirty = true;
1775            old_value
1776        };
1777        self.dirty = true;
1778        self.notify_attribute(idx, "class", Some(&old_value));
1779    }
1780    pub fn class_remove(&mut self, idx: usize, cls: &str) {
1781        let old_value = {
1782            let Some(n) = self.nodes.get_mut(idx) else {
1783                return;
1784            };
1785            let before = n.classes.len();
1786            let old_value = n.classes.join(" ");
1787            n.classes.retain(|c| c != cls);
1788            if n.classes.len() == before {
1789                return;
1790            }
1791            n.classes_dirty = true;
1792            old_value
1793        };
1794        self.dirty = true;
1795        self.notify_attribute(idx, "class", Some(&old_value));
1796    }
1797    pub fn class_toggle(&mut self, idx: usize, cls: &str) -> bool {
1798        if self.class_contains(idx, cls) {
1799            self.class_remove(idx, cls);
1800            false
1801        } else {
1802            self.class_add(idx, cls);
1803            true
1804        }
1805    }
1806    pub fn class_contains(&self, idx: usize, cls: &str) -> bool {
1807        self.nodes
1808            .get(idx)
1809            .map(|n| n.classes.iter().any(|c| c == cls))
1810            .unwrap_or(false)
1811    }
1812
1813    /// `<select>`配下の`<option>`要素を全て集める(`<optgroup>`でグループ化
1814    /// された`<option>`も含む再帰探索。丸ごと未対応だった。以前は直接の子
1815    /// のみを見る実装だったため、`<select><optgroup label="Fruits">
1816    /// <option>Apple</option></optgroup></select>`という非常によく使われる
1817    /// カテゴリ分けパターンで、`<optgroup>`配下の`<option>`が`select.
1818    /// options`/フォーム送信のどちらからも一切見えなくなる実害のある
1819    /// バグだった。`<option>`要素自身は子に更に`<option>`を持たない
1820    /// (仕様上そのような入れ子は無意味)ため、`<option>`に達したら
1821    /// そこで再帰を打ち切る。2026-07-17 発見・実装)。
1822    fn collect_descendant_options(&self, idx: usize, out: &mut Vec<usize>) {
1823        let Some(n) = self.nodes.get(idx) else { return };
1824        for &c in &n.children {
1825            let Some(cn) = self.nodes.get(c) else { continue };
1826            if cn.tag == "option" {
1827                out.push(c);
1828            } else {
1829                self.collect_descendant_options(c, out);
1830            }
1831        }
1832    }
1833    /// `<select>` の option 子要素一覧(`<optgroup>`配下も含む)。`select.
1834    /// options`(丸ごと未対応だった。動的にドロップダウンを構築する定番
1835    /// イディオムで使われる)向けに公開する。
1836    pub fn select_options(&self, idx: usize) -> Vec<usize> {
1837        let mut out = Vec::new();
1838        self.collect_descendant_options(idx, &mut out);
1839        out
1840    }
1841    /// option 要素の実効値: `value` 属性があればそれ、無ければテキスト内容
1842    /// (仕様どおり。`collect_form_data` の select フォールバックと同じ規則)。
1843    fn option_value(&self, opt_idx: usize) -> String {
1844        self.get_attr(opt_idx, "value").unwrap_or_else(|| {
1845            self.nodes
1846                .get(opt_idx)
1847                .map(|n| n.text_override.clone().unwrap_or_else(|| n.initial_text.clone()))
1848                .unwrap_or_default()
1849        })
1850    }
1851    /// `select.selectedOptions`(`<select multiple>` で選択中の option 一覧を得る
1852    /// 定番イディオム。`FormData` の multi-select 対応と同じく `selected` 属性を
1853    /// 真実源とする)。単一選択の `<select>` でも仕様どおり動作する
1854    /// (`selected` が無ければ既定選択の最初の option を1件返す)。
1855    pub fn selected_option_indices(&self, idx: usize) -> Vec<usize> {
1856        let options = self.select_options(idx);
1857        let selected: Vec<usize> = options.iter().copied().filter(|&o| self.has_attr(o, "selected")).collect();
1858        if !selected.is_empty() {
1859            selected
1860        } else {
1861            options.first().copied().into_iter().collect()
1862        }
1863    }
1864    /// `select.value`(丸ごと未対応だった。`<select>` 自身に `value` 属性は無く、
1865    /// 選択状態は子の `<option selected>` が持つため、汎用の属性直結 getter では
1866    /// 常に空文字列になっていた)。`selected` な option を優先し、無ければ
1867    /// HTML の既定選択どおり最初の option にフォールバックする。
1868    pub fn select_effective_value(&self, idx: usize) -> String {
1869        let options = self.select_options(idx);
1870        let target = options
1871            .iter()
1872            .copied()
1873            .find(|&o| self.has_attr(o, "selected"))
1874            .or_else(|| options.first().copied());
1875        match target {
1876            Some(o) => self.option_value(o),
1877            None => String::new(),
1878        }
1879    }
1880    /// フォーム系コントロールの「現在の実効値」を文字列で解決する。
1881    /// `checkValidity()`/`.validationMessage`/`.validity`が素朴に
1882    /// `get_attr(idx,"value")`だけを見ており、`value`属性を持たない
1883    /// `<select>`(選択状態は子`<option selected>`が持つ)や、初期値が
1884    /// `value`属性ではなくテキストノードで表現される`<textarea>`では
1885    /// 常に空文字列として(=未入力として)誤って検証されてしまうバグの
1886    /// 共通修正用。`.value`ゲッター(`dom_props.rs`)と同じフォールバック
1887    /// 規則をここに一本化する。
1888    pub fn effective_form_value(&self, idx: usize) -> String {
1889        let tag = self.nodes.get(idx).map(|n| n.tag.as_str()).unwrap_or("");
1890        if tag == "select" {
1891            self.select_effective_value(idx)
1892        } else if tag == "textarea" {
1893            self.get_attr(idx, "value").unwrap_or_else(|| {
1894                self.nodes
1895                    .get(idx)
1896                    .map(|n| n.initial_text.clone())
1897                    .unwrap_or_default()
1898            })
1899        } else {
1900            self.get_attr(idx, "value").unwrap_or_default()
1901        }
1902    }
1903    /// `select.value = v`(丸ごと未対応だった)。value が一致する最初の option へ
1904    /// 選択を移す(他の option の `selected` は解除。仕様どおり単一選択)。
1905    pub fn set_select_value(&mut self, idx: usize, val: &str) {
1906        let options = self.select_options(idx);
1907        for &o in &options {
1908            if self.option_value(o) == val {
1909                self.set_attr(o, "selected", "selected");
1910            } else {
1911                self.remove_attr(o, "selected");
1912            }
1913        }
1914        self.dirty = true;
1915    }
1916    /// `select.selectedIndex`(丸ごと未対応だった)。`selected` な option の位置、
1917    /// 無ければ option が1つ以上あれば `0`(既定選択)、無ければ `-1`。
1918    pub fn select_selected_index(&self, idx: usize) -> i32 {
1919        let options = self.select_options(idx);
1920        match options.iter().position(|&o| self.has_attr(o, "selected")) {
1921            Some(p) => p as i32,
1922            None if !options.is_empty() => 0,
1923            None => -1,
1924        }
1925    }
1926    /// `select.selectedIndex = n`。
1927    pub fn set_select_selected_index(&mut self, idx: usize, n: i32) {
1928        let options = self.select_options(idx);
1929        for (i, &o) in options.iter().enumerate() {
1930            if i as i32 == n {
1931                self.set_attr(o, "selected", "selected");
1932            } else {
1933                self.remove_attr(o, "selected");
1934            }
1935        }
1936        self.dirty = true;
1937    }
1938    /// `select.length = n`(HTML5。`select.options.length`と同じ値を返す
1939    /// ゲッター兼、`n`より末尾のoptionを切り捨てる/不足分を空の`<option>`で
1940    /// 埋めるセッター。丸ごと未対応だった)。仕様どおり、新しい長さが現在より
1941    /// 小さければ末尾から削除、大きければ空の`<option>`を追加する。
1942    pub fn set_select_length(&mut self, idx: usize, n: usize) {
1943        let options = self.select_options(idx);
1944        if n < options.len() {
1945            for &o in &options[n..] {
1946                if let Some(parent) = self.nodes.get(o).and_then(|node| node.parent) {
1947                    self.remove_child(parent, o);
1948                }
1949            }
1950        } else {
1951            for _ in options.len()..n {
1952                let opt = self.create_element("option");
1953                self.append_child(idx, opt);
1954            }
1955        }
1956        self.dirty = true;
1957    }
1958    /// `option.index`(HTML5。自身が属する`<select>`内での0始まりの位置。
1959    /// `select.selectedIndex`の対になる、`<option>`側のIDL属性だが丸ごと
1960    /// 未対応だった)。`<select>`祖先が無い場合は仕様どおり`0`(要素が
1961    /// 「optionのリスト」に属さない場合の既定値。`<datalist>`配下は
1962    /// 対象外の簡略実装)。
1963    pub fn option_index(&self, idx: usize) -> i32 {
1964        match self.closest_tag(idx, "select") {
1965            Some(sel) => self
1966                .select_options(sel)
1967                .iter()
1968                .position(|&o| o == idx)
1969                .map(|p| p as i32)
1970                .unwrap_or(0),
1971            None => 0,
1972        }
1973    }
1974
1975    pub fn get_attr(&self, idx: usize, name: &str) -> Option<String> {
1976        let n = self.nodes.get(idx)?;
1977        match name {
1978            "id" => Some(n.id.clone()),
1979            "class" => Some(n.classes.join(" ")),
1980            _ => n.attrs.get(name).cloned(),
1981        }
1982    }
1983    pub fn set_attr(&mut self, idx: usize, name: &str, val: &str) {
1984        let old_value = self.get_attr(idx, name);
1985        if let Some(n) = self.nodes.get_mut(idx) {
1986            match name {
1987                "id" => n.id = val.to_string(),
1988                "class" => {
1989                    n.classes = val.split_whitespace().map(String::from).collect();
1990                    n.classes_dirty = true;
1991                }
1992                _ => {
1993                    n.attrs.insert(name.to_string(), val.to_string());
1994                }
1995            }
1996            self.dirty = true;
1997        }
1998        self.notify_attribute(idx, name, old_value.as_deref());
1999    }
2000
2001    /// 属性が存在するか(空値の boolean 属性も存在として true。id/class は専用フィールド)。
2002    pub fn has_attr(&self, idx: usize, name: &str) -> bool {
2003        match self.nodes.get(idx) {
2004            Some(n) => match name {
2005                "id" => !n.id.is_empty(),
2006                "class" => !n.classes.is_empty(),
2007                _ => n.attrs.contains_key(name),
2008            },
2009            None => false,
2010        }
2011    }
2012
2013    /// 属性を削除(id/class フィールドも同期)。
2014    pub fn remove_attr(&mut self, idx: usize, name: &str) {
2015        // `set_attr` は属性変化を MutationObserver へ通知していたが、`remove_attr`
2016        // だけこの通知呼び出しが漏れていた非対称ペア(`removeAttribute()` も仕様上
2017        // "attributes" タイプの MutationRecord を発行すべき)。
2018        let old_value = self.get_attr(idx, name);
2019        if let Some(n) = self.nodes.get_mut(idx) {
2020            match name {
2021                "id" => n.id = String::new(),
2022                "class" => {
2023                    n.classes.clear();
2024                    n.classes_dirty = true;
2025                }
2026                _ => {
2027                    n.attrs.remove(name);
2028                }
2029            }
2030            self.dirty = true;
2031        }
2032        self.notify_attribute(idx, name, old_value.as_deref());
2033    }
2034
2035    /// `<input type="radio">` の排他選択(同じ `name` を持つグループ内で1つだけが
2036    /// `checked` になる、というラジオボタンの最も基本的な仕様上の挙動)。`idx` を
2037    /// checked にする際、同じ `name` を持つ他の radio 全ての `checked` 属性を外す。
2038    /// フォーム所有者(`<form>` 単位のスコープ)は区別せず、同じ `name` を持つ
2039    /// document 内の全 radio を対象にする簡略実装。
2040    /// `<details name="...">`(HTML5。同じ`name`を持つ`<details>`同士は
2041    /// ラジオボタングループのように排他的に開閉する——1つを開くと同じ
2042    /// グループの他の`<details>`は自動的に閉じる——仕様が丸ごと未対応
2043    /// だった。`uncheck_radio_group_siblings`と同じ「`name`属性が一致する
2044    /// 兄弟(ドキュメント全体、DOM上の兄弟関係は問わない)を探して状態を
2045    /// 揃える」パターンをそのまま適用する。2026-07-18 発見・実装)。
2046    pub fn close_details_group_siblings(&mut self, idx: usize) -> alloc::vec::Vec<usize> {
2047        let name = match self.nodes.get(idx).and_then(|n| n.attrs.get("name")) {
2048            Some(n) if !n.is_empty() => n.clone(),
2049            _ => return alloc::vec::Vec::new(),
2050        };
2051        let siblings: alloc::vec::Vec<usize> = self
2052            .nodes
2053            .iter()
2054            .enumerate()
2055            .filter(|(i, n)| {
2056                *i != idx
2057                    && n.tag == "details"
2058                    && n.attrs.get("name").map(|s| s.as_str()) == Some(name.as_str())
2059                    && n.attrs.contains_key("open")
2060            })
2061            .map(|(i, _)| i)
2062            .collect();
2063        for &i in &siblings {
2064            self.remove_attr(i, "open");
2065        }
2066        siblings
2067    }
2068
2069    pub fn uncheck_radio_group_siblings(&mut self, idx: usize) {
2070        let name = match self.nodes.get(idx).and_then(|n| n.attrs.get("name")) {
2071            Some(n) if !n.is_empty() => n.clone(),
2072            _ => return,
2073        };
2074        let siblings: Vec<usize> = self
2075            .nodes
2076            .iter()
2077            .enumerate()
2078            .filter(|(i, n)| {
2079                *i != idx
2080                    && n.tag == "input"
2081                    && n.attrs.get("type").map(|t| t.as_str()) == Some("radio")
2082                    && n.attrs.get("name").map(|s| s.as_str()) == Some(name.as_str())
2083            })
2084            .map(|(i, _)| i)
2085            .collect();
2086        for i in siblings {
2087            self.remove_attr(i, "checked");
2088            self.set_attr(i, "_live_checked", "false");
2089        }
2090    }
2091
2092    /// 単一ノードがセレクタ(コンパウンド + 結合子、カンマ区切りは OR)にマッチするか。
2093    /// `matches()`/`closest()`/`querySelector(All)` 共通の実装本体。以前は `#id`/`.class`/
2094    /// `tag`/`*` の単純セレクタしか認識せず、`div.active`(コンパウンド)や `[data-x]`
2095    /// (属性セレクタ)、`.a > .b`(結合子)を渡すと文字列全体を tag 名として比較する
2096    /// 分岐に落ち、常に false になる静かな破壊バグだった(`:not()`/`:nth-child()`/
2097    /// `:is()`/`:where()` 等の疑似クラスは引き続き非対応。改修規模を絞るための
2098    /// 意図的な割り切り)。
2099    pub fn node_matches(&self, idx: usize, selector: &str) -> bool {
2100        self.node_matches_scoped(idx, selector, None)
2101    }
2102
2103    /// `node_matches` の `:scope` 対応版。`scope_root` に `element.
2104    /// querySelectorAll`/`matches`/`closest` 等の起点ノードを渡すと、セレクタ中の
2105    /// `:scope` 疑似クラス(Selectors Level 4。丸ごと未対応だった)がそのノードに
2106    /// のみ一致するようになる。`scope_root` が `None`(`document.querySelectorAll`
2107    /// 等、起点を持たない文脈)の場合は `:scope` は常に不一致として扱う簡略実装。
2108    pub fn node_matches_scoped(&self, idx: usize, selector: &str, scope_root: Option<usize>) -> bool {
2109        if self.nodes.get(idx).map(|n| n.is_text).unwrap_or(true) {
2110            return false;
2111        }
2112        for part in split_top_level_commas(selector) {
2113            let steps = build_steps(&tokenize_selector_group(part.trim()));
2114            if steps.is_empty() {
2115                continue;
2116            }
2117            let last = steps.len() - 1;
2118            if self.compound_matches(idx, &steps[last].compound, scope_root)
2119                && self.match_selector_chain(idx, &steps, last, scope_root)
2120            {
2121                return true;
2122            }
2123        }
2124        false
2125    }
2126
2127    /// `idx` がコンパウンドセレクタ(タグ/`#id`/`.class`/`[attr...]`)に一致するか。
2128    fn compound_matches(&self, idx: usize, c: &CompoundSel, scope_root: Option<usize>) -> bool {
2129        if c.is_scope && Some(idx) != scope_root {
2130            return false;
2131        }
2132        let n = match self.nodes.get(idx) {
2133            Some(n) if !n.is_text => n,
2134            _ => return false,
2135        };
2136        if let Some(tag) = &c.tag {
2137            if &n.tag != tag {
2138                return false;
2139            }
2140        }
2141        if let Some(id) = &c.id {
2142            if &n.id != id {
2143                return false;
2144            }
2145        }
2146        if !c.classes.iter().all(|cls| n.classes.iter().any(|nc| nc == cls)) {
2147            return false;
2148        }
2149        for attr in &c.attrs {
2150            let val = match n.attrs.get(&attr.name) {
2151                Some(v) => v,
2152                None => return false,
2153            };
2154            let ok = match attr.op {
2155                AttrOp::Exists => true,
2156                AttrOp::Eq => val == &attr.value,
2157                AttrOp::Prefix => val.starts_with(attr.value.as_str()),
2158                AttrOp::Suffix => val.ends_with(attr.value.as_str()),
2159                AttrOp::Contains => !attr.value.is_empty() && val.contains(attr.value.as_str()),
2160                AttrOp::Word => val.split_whitespace().any(|w| w == attr.value),
2161            };
2162            if !ok {
2163                return false;
2164            }
2165        }
2166        for not_sel in &c.not {
2167            if self.compound_matches(idx, not_sel, scope_root) {
2168                return false;
2169            }
2170        }
2171        if !c.is_list.is_empty()
2172            && !c.is_list.iter().any(|s| self.compound_matches(idx, s, scope_root))
2173        {
2174            return false;
2175        }
2176        if !c.pos.is_empty() {
2177            let (position, count) = self.element_sibling_position(idx);
2178            for p in &c.pos {
2179                let ok = match p {
2180                    PosPseudo::First => position == 1,
2181                    PosPseudo::Last => position == count,
2182                    PosPseudo::Only => position == 1 && count == 1,
2183                    PosPseudo::Nth(f) => nth_formula_matches(*f, position),
2184                    PosPseudo::NthLast(f) => nth_formula_matches(*f, count - position + 1),
2185                };
2186                if !ok {
2187                    return false;
2188                }
2189            }
2190        }
2191        if !c.pos_of_type.is_empty() {
2192            let (position, count) = self.element_type_sibling_position(idx);
2193            for p in &c.pos_of_type {
2194                let ok = match p {
2195                    PosPseudo::First => position == 1,
2196                    PosPseudo::Last => position == count,
2197                    PosPseudo::Only => position == 1 && count == 1,
2198                    PosPseudo::Nth(f) => nth_formula_matches(*f, position),
2199                    PosPseudo::NthLast(f) => nth_formula_matches(*f, count - position + 1),
2200                };
2201                if !ok {
2202                    return false;
2203                }
2204            }
2205        }
2206        true
2207    }
2208
2209    /// `idx` の「要素兄弟のみを数えた」1始まりの位置と、要素兄弟の総数を返す
2210    /// (テキストノードは数えない。CSS の `:nth-child()` 等と同じ規則)。
2211    fn element_sibling_position(&self, idx: usize) -> (usize, usize) {
2212        let parent = match self.nodes.get(idx).and_then(|n| n.parent) {
2213            Some(p) => p,
2214            None => return (1, 1),
2215        };
2216        let siblings = match self.nodes.get(parent) {
2217            Some(n) => &n.children,
2218            None => return (1, 1),
2219        };
2220        let elements: Vec<usize> = siblings
2221            .iter()
2222            .filter(|&&c| !self.nodes.get(c).map(|n| n.is_text).unwrap_or(true))
2223            .copied()
2224            .collect();
2225        let count = elements.len();
2226        let position = elements.iter().position(|&c| c == idx).map(|p| p + 1).unwrap_or(0);
2227        (position, count)
2228    }
2229
2230    /// `element_sibling_position` と同じだが、同じタグ名の兄弟のみを数える
2231    /// (`:first-of-type`/`:last-of-type`/`:nth-of-type()` 用)。
2232    fn element_type_sibling_position(&self, idx: usize) -> (usize, usize) {
2233        let tag = match self.nodes.get(idx) {
2234            Some(n) => n.tag.clone(),
2235            None => return (1, 1),
2236        };
2237        let parent = match self.nodes.get(idx).and_then(|n| n.parent) {
2238            Some(p) => p,
2239            None => return (1, 1),
2240        };
2241        let siblings = match self.nodes.get(parent) {
2242            Some(n) => &n.children,
2243            None => return (1, 1),
2244        };
2245        let same_type: Vec<usize> = siblings
2246            .iter()
2247            .filter(|&&c| self.nodes.get(c).map(|n| !n.is_text && n.tag == tag).unwrap_or(false))
2248            .copied()
2249            .collect();
2250        let count = same_type.len();
2251        let position = same_type.iter().position(|&c| c == idx).map(|p| p + 1).unwrap_or(0);
2252        (position, count)
2253    }
2254
2255    /// `idx` は `steps[step_i]` に既に一致した状態で呼ばれる。`steps[0..step_i]` を
2256    /// 結合子に従って祖先/兄弟方向へ再帰的に検証する。
2257    fn match_selector_chain(
2258        &self,
2259        idx: usize,
2260        steps: &[SelStep],
2261        step_i: usize,
2262        scope_root: Option<usize>,
2263    ) -> bool {
2264        if step_i == 0 {
2265            return true;
2266        }
2267        let combinator = match steps[step_i].combinator {
2268            Some(c) => c,
2269            None => return true,
2270        };
2271        match combinator {
2272            Combinator::Descendant => {
2273                let mut cur = self.nodes.get(idx).and_then(|n| n.parent);
2274                let mut guard = 0;
2275                while let Some(p) = cur {
2276                    if self.compound_matches(p, &steps[step_i - 1].compound, scope_root)
2277                        && self.match_selector_chain(p, steps, step_i - 1, scope_root)
2278                    {
2279                        return true;
2280                    }
2281                    cur = self.nodes.get(p).and_then(|n| n.parent);
2282                    guard += 1;
2283                    if guard > self.nodes.len() {
2284                        break;
2285                    }
2286                }
2287                false
2288            }
2289            Combinator::Child => match self.nodes.get(idx).and_then(|n| n.parent) {
2290                Some(p) => {
2291                    self.compound_matches(p, &steps[step_i - 1].compound, scope_root)
2292                        && self.match_selector_chain(p, steps, step_i - 1, scope_root)
2293                }
2294                None => false,
2295            },
2296            Combinator::Adjacent => match self.prev_element_sibling(idx) {
2297                Some(p) => {
2298                    self.compound_matches(p, &steps[step_i - 1].compound, scope_root)
2299                        && self.match_selector_chain(p, steps, step_i - 1, scope_root)
2300                }
2301                None => false,
2302            },
2303            Combinator::General => {
2304                for p in self.preceding_element_siblings(idx) {
2305                    if self.compound_matches(p, &steps[step_i - 1].compound, scope_root)
2306                        && self.match_selector_chain(p, steps, step_i - 1, scope_root)
2307                    {
2308                        return true;
2309                    }
2310                }
2311                false
2312            }
2313        }
2314    }
2315
2316    /// `idx` の直前の要素兄弟(テキストノードは飛ばす)。
2317    fn prev_element_sibling(&self, idx: usize) -> Option<usize> {
2318        let parent = self.nodes.get(idx)?.parent?;
2319        let siblings = &self.nodes.get(parent)?.children;
2320        let pos = siblings.iter().position(|&c| c == idx)?;
2321        siblings[..pos].iter().rev().find(|&&c| !self.nodes.get(c).map(|n| n.is_text).unwrap_or(true)).copied()
2322    }
2323
2324    /// `idx` より前にある全ての要素兄弟(テキストノードは飛ばす、ドキュメント順)。
2325    fn preceding_element_siblings(&self, idx: usize) -> Vec<usize> {
2326        let parent = match self.nodes.get(idx).and_then(|n| n.parent) {
2327            Some(p) => p,
2328            None => return Vec::new(),
2329        };
2330        let siblings = match self.nodes.get(parent) {
2331            Some(n) => &n.children,
2332            None => return Vec::new(),
2333        };
2334        let pos = match siblings.iter().position(|&c| c == idx) {
2335            Some(p) => p,
2336            None => return Vec::new(),
2337        };
2338        siblings[..pos]
2339            .iter()
2340            .filter(|&&c| !self.nodes.get(c).map(|n| n.is_text).unwrap_or(true))
2341            .copied()
2342            .collect()
2343    }
2344
2345    /// anc が idx の祖先か(idx 自身は含まない)。
2346    pub fn is_ancestor_of(&self, anc: usize, idx: usize) -> bool {
2347        let mut cur = idx;
2348        let mut guard = 0;
2349        while let Some(p) = self.nodes.get(cur).and_then(|n| n.parent) {
2350            if p == anc {
2351                return true;
2352            }
2353            cur = p;
2354            guard += 1;
2355            if guard > self.nodes.len() {
2356                break;
2357            }
2358        }
2359        false
2360    }
2361
2362    /// `Node.compareDocumentPosition(other)`(DOM標準)が丸ごと未対応だった。
2363    /// `this_idx`(reference node)から見た `other_idx` の位置関係をビットマスクで返す
2364    /// (`DOCUMENT_POSITION_*` 定数は呼び出し側 `builtins.rs` で定義)。祖先/子孫関係は
2365    /// 既存の `is_ancestor_of` で判定し、それ以外(兄弟や別分岐)は両者の祖先鎖を根まで
2366    /// 辿って最も近い共通祖先(LCA)の子リスト内での順序を比較する。根が異なる
2367    /// (disconnected)場合、仕様上 PRECEDING/FOLLOWING の割当ては実装依存でよいため
2368    /// ノードindexの大小で決定的に決める。
2369    pub fn document_position(&self, this_idx: usize, other_idx: usize) -> u32 {
2370        const DISCONNECTED: u32 = 0x01;
2371        const PRECEDING: u32 = 0x02;
2372        const FOLLOWING: u32 = 0x04;
2373        const CONTAINS: u32 = 0x08;
2374        const CONTAINED_BY: u32 = 0x10;
2375        const IMPLEMENTATION_SPECIFIC: u32 = 0x20;
2376
2377        if this_idx == other_idx {
2378            return 0;
2379        }
2380        if self.is_ancestor_of(other_idx, this_idx) {
2381            // other は this の祖先(other が this を含む)。
2382            return CONTAINS | PRECEDING;
2383        }
2384        if self.is_ancestor_of(this_idx, other_idx) {
2385            // other は this の子孫(this が other を含む)。
2386            return CONTAINED_BY | FOLLOWING;
2387        }
2388        let chain_of = |start: usize| -> Vec<usize> {
2389            let mut chain = alloc::vec![start];
2390            let mut cur = start;
2391            let mut guard = 0;
2392            while let Some(p) = self.nodes.get(cur).and_then(|n| n.parent) {
2393                chain.push(p);
2394                cur = p;
2395                guard += 1;
2396                if guard > self.nodes.len() {
2397                    break;
2398                }
2399            }
2400            chain.reverse(); // ルート→自身の順に。
2401            chain
2402        };
2403        let chain_a = chain_of(this_idx);
2404        let chain_b = chain_of(other_idx);
2405        if chain_a.first() != chain_b.first() {
2406            // 根が異なる = disconnected。順序は実装依存でよいので index で決定的に決める。
2407            let order = if this_idx < other_idx { FOLLOWING } else { PRECEDING };
2408            return DISCONNECTED | IMPLEMENTATION_SPECIFIC | order;
2409        }
2410        let mut i = 0;
2411        while i < chain_a.len() && i < chain_b.len() && chain_a[i] == chain_b[i] {
2412            i += 1;
2413        }
2414        if i == 0 || i >= chain_a.len() || i >= chain_b.len() {
2415            // 分岐点が見つからない(理論上到達しないはずだが防御的に)。
2416            return DISCONNECTED | IMPLEMENTATION_SPECIFIC;
2417        }
2418        let lca = chain_a[i - 1];
2419        let child_a = chain_a[i];
2420        let child_b = chain_b[i];
2421        let siblings = match self.nodes.get(lca) {
2422            Some(n) => &n.children,
2423            None => return DISCONNECTED | IMPLEMENTATION_SPECIFIC,
2424        };
2425        let pos_a = siblings.iter().position(|&c| c == child_a);
2426        let pos_b = siblings.iter().position(|&c| c == child_b);
2427        match (pos_a, pos_b) {
2428            (Some(pa), Some(pb)) if pa < pb => FOLLOWING,
2429            (Some(pa), Some(pb)) if pa > pb => PRECEDING,
2430            _ => DISCONNECTED | IMPLEMENTATION_SPECIFIC,
2431        }
2432    }
2433
2434    /// idx 自身を含め祖先方向へ、指定タグの最初の要素を返す(`<form>` 解決等)。
2435    /// `element.offsetParent`(HTML5。丸ごと未対応だった)。祖先を遡り、算出
2436    /// `position` が `static` 以外の最初の要素を返す。無ければ `<body>`
2437    /// (見つからなければ `None` = 仕様どおり `null`)。
2438    pub fn offset_parent(&self, idx: usize) -> Option<usize> {
2439        let mut cur = self.nodes.get(idx).and_then(|n| n.parent);
2440        let mut body_idx = None;
2441        let mut guard = 0;
2442        while let Some(c) = cur {
2443            if self.nodes.get(c).map(|n| !n.is_text && n.tag == "body").unwrap_or(false) {
2444                body_idx = Some(c);
2445            }
2446            let pos = self.get_computed(c, "position");
2447            if pos == "absolute" || pos == "relative" || pos == "fixed" || pos == "sticky" {
2448                return Some(c);
2449            }
2450            cur = self.nodes.get(c).and_then(|n| n.parent);
2451            guard += 1;
2452            if guard > self.nodes.len() {
2453                break;
2454            }
2455        }
2456        body_idx
2457    }
2458
2459    pub fn closest_tag(&self, idx: usize, tag: &str) -> Option<usize> {
2460        let mut cur = Some(idx);
2461        let mut guard = 0;
2462        while let Some(c) = cur {
2463            if self
2464                .nodes
2465                .get(c)
2466                .map(|n| !n.is_text && n.tag == tag)
2467                .unwrap_or(false)
2468            {
2469                return Some(c);
2470            }
2471            cur = self.nodes.get(c).and_then(|n| n.parent);
2472            guard += 1;
2473            if guard > self.nodes.len() {
2474                break;
2475            }
2476        }
2477        None
2478    }
2479
2480    /// フォーム系コントロールが実際に属する`<form>`を解決する。HTML5仕様では
2481    /// `form="formId"`属性(`<form>`の子孫でなくても、その`id`で明示的に関連付ける
2482    /// 「form owner」機構)が祖先探索より優先される。以前は`closest_tag(idx,
2483    /// "form")`(祖先探索のみ)しか実装されておらず、`<form id="f1">`の外に置かれた
2484    /// `<input form="f1">`(フォーム外配置は`<form>`の視覚レイアウトを変えずに
2485    /// コントロールだけ離れた場所に置きたい場合の定番パターン)が
2486    /// `element.form`で`null`になり、`<form>.checkValidity()`/送信データ収集からも
2487    /// 漏れる、という兄弟ギャップだった。
2488    pub fn associated_form(&self, idx: usize) -> Option<usize> {
2489        if let Some(form_id) = self.get_attr(idx, "form") {
2490            if let Some(fi) = self.get_element_by_id(&form_id) {
2491                if self.nodes.get(fi).map(|n| n.tag == "form").unwrap_or(false) {
2492                    return Some(fi);
2493                }
2494            }
2495        }
2496        self.closest_tag(idx, "form")
2497    }
2498
2499    /// `<form id="f1">`に属する`tags`に含まれるタグの全フォームコントロールを
2500    /// 収集する。子孫に加えて、`form="f1"`属性でフォーム外から明示的に関連付けられた
2501    /// コントロール(`associated_form`と対になる「form owner」機構)も含む。
2502    /// `checkValidity()`/`FormData`収集/`form.elements`の各`<form>`祖先ループが
2503    /// フォーム外配置のコントロールを見落としていた兄弟ギャップの修正に使う
2504    /// (呼び出し側ごとに対象タグが異なる: `checkValidity`/`FormData`は
2505    /// `input`/`select`/`textarea`のみ、`form.elements`はそれに`button`も含む)。
2506    pub fn form_associated_controls(&self, form_idx: usize, tags: &[&str]) -> Vec<usize> {
2507        let mut out = self.descendants_by_tags(form_idx, tags);
2508        if let Some(form_id) = self.get_attr(form_idx, "id") {
2509            if !form_id.is_empty() {
2510                for (i, n) in self.nodes.iter().enumerate() {
2511                    if n.is_text || out.contains(&i) {
2512                        continue;
2513                    }
2514                    if !tags.contains(&n.tag.as_str()) {
2515                        continue;
2516                    }
2517                    if n.attrs.get("form").map(|v| v == &form_id).unwrap_or(false) {
2518                        out.push(i);
2519                    }
2520                }
2521            }
2522        }
2523        out
2524    }
2525
2526    /// ドキュメント順で最初の指定タグ要素。
2527    pub fn first_by_tag(&self, tag: &str) -> Option<usize> {
2528        (0..self.nodes.len())
2529            .find(|&i| !self.nodes[i].is_text && self.nodes[i].tag == tag && self.is_attached(i))
2530    }
2531
2532    /// 子のうち要素(非テキスト)ノードの index 一覧。
2533    pub fn element_children(&self, idx: usize) -> Vec<usize> {
2534        self.nodes
2535            .get(idx)
2536            .map(|n| {
2537                n.children
2538                    .iter()
2539                    .copied()
2540                    .filter(|c| self.nodes.get(*c).map(|cn| !cn.is_text).unwrap_or(false))
2541                    .collect()
2542            })
2543            .unwrap_or_default()
2544    }
2545
2546    /// `root` 以下(自身は含まない)の子孫のうち、tag が `tags` のいずれかに一致するものを
2547    /// 文書順で収集する。フォーム配下の入力欄列挙(バリデーション)に使用。
2548    /// `element.getElementsByTagName(tag)` 用の子孫走査。`tag` が `"*"` なら全要素。
2549    ///
2550    /// # なぜ文書全体の検索を使わないか
2551    ///
2552    /// `query_all` は `is_attached` で**文書に繋がっていないノードを除外**する。
2553    /// 仕様では `getElementsByTagName` は**文書への接続に関係なく**自分の子孫を
2554    /// 返すので、切り離した要素(`createElement` した直後など)では結果が
2555    /// 空になってしまう。
2556    ///
2557    /// jQuery 1.8.2 の機能検出はまさにこれをやる:
2558    /// 切り離した `<div>` に `innerHTML` を入れ、`getElementsByTagName("a")[0]`
2559    /// の `style` を触る。空が返ると `undefined` の `style` を読んで落ち、
2560    /// **jQuery 全体が初期化できなくなっていた**。
2561    ///
2562    /// なお `is_attached` は根(索引 0)まで遡る実装のため、**空の DOM では
2563    /// 最初に作った要素自身が索引 0 になり偶然「接続済み」になる**。
2564    /// 単体試験を空のランタイムで書くとこの不具合を再現できない
2565    /// (実際に 5 回取り逃がした)。試験は実ページの DOM で確かめること。
2566    pub fn descendants_by_tag_name(&self, root: usize, tag: &str) -> Vec<usize> {
2567        let want_all = tag == "*";
2568        let lower = tag.to_ascii_lowercase();
2569        let mut out = Vec::new();
2570        let mut stack: Vec<usize> = self.element_children(root);
2571        let mut i = 0;
2572        while i < stack.len() {
2573            let idx = stack[i];
2574            i += 1;
2575            if let Some(n) = self.nodes.get(idx) {
2576                if !n.is_text && (want_all || n.tag.eq_ignore_ascii_case(&lower)) {
2577                    out.push(idx);
2578                }
2579                for &c in &n.children {
2580                    if self.nodes.get(c).map(|cn| !cn.is_text).unwrap_or(false) {
2581                        stack.push(c);
2582                    }
2583                }
2584            }
2585        }
2586        out
2587    }
2588
2589    pub fn descendants_by_tags(&self, root: usize, tags: &[&str]) -> Vec<usize> {
2590        let mut out = Vec::new();
2591        let mut stack: Vec<usize> = self.element_children(root);
2592        // element_children は逆順処理だと順序が崩れるため、明示的に前順走査する。
2593        let mut i = 0;
2594        while i < stack.len() {
2595            let idx = stack[i];
2596            i += 1;
2597            if let Some(n) = self.nodes.get(idx) {
2598                if !n.is_text && tags.iter().any(|t| n.tag == *t) {
2599                    out.push(idx);
2600                }
2601                // 子をその場に展開(文書順を保つため idx の直後ではなく末尾へ)。
2602                for &c in &n.children {
2603                    if self.nodes.get(c).map(|cn| !cn.is_text).unwrap_or(false) {
2604                        stack.push(c);
2605                    }
2606                }
2607            }
2608        }
2609        out
2610    }
2611
2612    /// `<fieldset disabled>`の子孫コントロールは、自身に`disabled`属性が無くても
2613    /// 暗黙に無効化される(HTML5仕様の「fieldset disabling」)。以前は要素自身の
2614    /// `disabled`属性の有無しか見ておらず、`<fieldset disabled>`配下の`<input>`は
2615    /// `.disabled`が常に`false`、制約バリデーションも無効化されずに常に対象内の
2616    /// ままになっていた。祖先を`parent`で遡り`<fieldset disabled>`が1つでも
2617    /// あれば無効とみなす(`<fieldset>`の最初の子`<legend>`直下のコントロールは
2618    /// 無効化対象外という仕様上の例外は、この処理系のフォーム系テストでは実用上
2619    /// ほぼ現れないため今回は簡略化して見送る)。
2620    pub fn is_disabled(&self, idx: usize) -> bool {
2621        let n = match self.nodes.get(idx) {
2622            Some(n) => n,
2623            None => return false,
2624        };
2625        if n.attrs.contains_key("disabled") {
2626            return true;
2627        }
2628        let mut cur = n.parent;
2629        while let Some(i) = cur {
2630            let anc = match self.nodes.get(i) {
2631                Some(a) => a,
2632                None => break,
2633            };
2634            if anc.tag == "fieldset" && anc.attrs.contains_key("disabled") {
2635                return true;
2636            }
2637            cur = anc.parent;
2638        }
2639        false
2640    }
2641
2642    /// `ValidityState` の各フラグを個別に計算する(`validate_field` は最初に見つかった
2643    /// 違反のメッセージ1件しか返さないため、`.validity.patternMismatch` のような特定
2644    /// 制約の狙い撃ちには使えなかった)。`validate_field` と同じ規則を、短絡させずに
2645    /// 全項目評価する。
2646    pub fn validity_flags(&self, idx: usize, value: &str) -> ValidityFlags {
2647        let mut f = ValidityFlags::default();
2648        let n = match self.nodes.get(idx) {
2649            Some(n) => n,
2650            None => return f,
2651        };
2652        // 仕様上「制約検証の対象候補」になれるのは`button`/`input`/`select`/
2653        // `textarea`の4タグのみ(`output`は明示的に対象外、`fieldset`/
2654        // `object`等の非フォームコントロールも対象外)。以前はタグを一切
2655        // チェックしていなかったため、`<output>`/`<div>`のような要素に
2656        // 誤って`required`属性を付けると`valueMissing`が立ってしまう
2657        // バグだった(`willValidate`側で発見・修正した同根のバグ。
2658        // 2026-07-18 発見・修正)。
2659        if n.is_text
2660            || self.is_disabled(idx)
2661            || !matches!(n.tag.as_str(), "button" | "input" | "select" | "textarea")
2662        {
2663            return f;
2664        }
2665        let input_type = n
2666            .attrs
2667            .get("type")
2668            .map(|s| s.trim().to_lowercase())
2669            .unwrap_or_default();
2670        if matches!(
2671            input_type.as_str(),
2672            "submit" | "button" | "reset" | "hidden" | "image"
2673        ) {
2674            return f;
2675        }
2676        f.custom_error = n
2677            .attrs
2678            .get("_custom_validity")
2679            .map(|s| !s.is_empty())
2680            .unwrap_or(false);
2681        let trimmed = value.trim();
2682        // checkbox/radio は「値」でなく「チェック状態」で required を判定する
2683        // (`validate_field` と同じ規則。詳細はそちらのコメント参照)。
2684        f.value_missing = n.attrs.contains_key("required") && !self.required_satisfied(n, &input_type, trimmed);
2685        if matches!(input_type.as_str(), "checkbox" | "radio") || value.is_empty() {
2686            return f; // checkbox/radio、または空値は required 以外のチェックを適用しない(仕様どおり)。
2687        }
2688        let char_len = value.chars().count();
2689        if let Some(ml) = n.attrs.get("minlength").and_then(|v| v.trim().parse::<usize>().ok()) {
2690            f.too_short = char_len < ml;
2691        }
2692        if let Some(ml) = n.attrs.get("maxlength").and_then(|v| v.trim().parse::<usize>().ok()) {
2693            f.too_long = char_len > ml;
2694        }
2695        match input_type.as_str() {
2696            "email" => f.type_mismatch = !Self::is_valid_email_value(n, value),
2697            "url" => {
2698                f.type_mismatch = !(value.starts_with("http://") || value.starts_with("https://"))
2699            }
2700            // `type="date"`/`type="month"`の形式チェックが丸ごと未対応だった
2701            // (`input.valueAsDate`は既に`input_value_as_date_ms()`で`YYYY-MM-DD`/
2702            // `YYYY-MM`の妥当性を判定していたのに、`validity.typeMismatch`は
2703            // `email`/`url`だけしか見ておらず、不正な日付文字列を代入しても
2704            // 常に`typeMismatch: false`だった)。新規パーサは書かず既存関数を
2705            // そのまま再利用する。
2706            // `min`/`max`(日付の範囲制約。HTML5仕様上`type="date"`/`type="month"`も
2707            // `type="number"`同様に`min`/`max`属性で範囲を制約できるが、
2708            // `range_overflow`/`range_underflow`は元々`number`/`range`型でしか
2709            // 計算されておらず、日付型には丸ごと存在しなかった)は
2710            // `input_value_as_date_ms()`でms値に変換して比較するだけで、
2711            // 文字列比較用の新規ロジックは不要(ISO 8601形式は辞書式順序と
2712            // 時系列順序が一致するが、あえて数値のms比較で仕様どおりに扱う)。
2713            // `type="datetime-local"`も`input_value_as_date_ms()`が既に
2714            // `YYYY-MM-DDTHH:MM`形式をサポートしているため、`date`/`month`と
2715            // 同じ扱いに含めるだけで対応できる(`week`/`time`はこの関数自体が
2716            // 対応していないため対象外のまま)。
2717            "date" | "month" | "datetime-local" => match super::builtins::input_value_as_date_ms(&input_type, value) {
2718                Some(ms) => {
2719                    if let Some(min) = n
2720                        .attrs
2721                        .get("min")
2722                        .and_then(|v| super::builtins::input_value_as_date_ms(&input_type, v))
2723                    {
2724                        f.range_underflow = ms < min;
2725                    }
2726                    if let Some(max) = n
2727                        .attrs
2728                        .get("max")
2729                        .and_then(|v| super::builtins::input_value_as_date_ms(&input_type, v))
2730                    {
2731                        f.range_overflow = ms > max;
2732                    }
2733                }
2734                None => f.type_mismatch = true,
2735            },
2736            // `type="range"`もmin/max/stepの数値制約検証は`type="number"`と全く同じ
2737            // (`input.stepUp()`/`.stepDown()`/`.valueAsNumber`は既に両方を同列に
2738            // 扱っていたのに、`validity.rangeOverflow`等の制約検証だけ`number`のみに
2739            // 限定されており、range型ではJSから範囲外の値を代入しても常に
2740            // `range_overflow`/`range_underflow`/`step_mismatch`が`false`固定になる
2741            // 兄弟ギャップだった)。
2742            "number" | "range" => match value.trim().parse::<f64>() {
2743                Ok(num) => {
2744                    if let Some(min) = n.attrs.get("min").and_then(|v| v.trim().parse::<f64>().ok()) {
2745                        f.range_underflow = num < min;
2746                    }
2747                    if let Some(max) = n.attrs.get("max").and_then(|v| v.trim().parse::<f64>().ok()) {
2748                        f.range_overflow = num > max;
2749                    }
2750                    let step_attr = n.attrs.get("step").map(|s| s.trim());
2751                    if step_attr != Some("any") {
2752                        let step = step_attr
2753                            .and_then(|s| s.parse::<f64>().ok())
2754                            .filter(|s| *s > 0.0)
2755                            .unwrap_or(1.0);
2756                        let base = n
2757                            .attrs
2758                            .get("min")
2759                            .and_then(|v| v.trim().parse::<f64>().ok())
2760                            .unwrap_or(0.0);
2761                        let steps = (num - base) / step;
2762                        f.step_mismatch = libm::fabs(steps - libm::round(steps)) > 1e-9;
2763                    }
2764                }
2765                Err(_) => f.bad_input = true,
2766            },
2767            _ => {}
2768        }
2769        if let Some(pat) = n.attrs.get("pattern") {
2770            if !pat.is_empty() {
2771                f.pattern_mismatch = !Self::pattern_matches(pat, value);
2772            }
2773        }
2774        f
2775    }
2776
2777    /// `required`制約が充足されているかを判定する。`checkbox`は自身の`checked`、
2778    /// `radio`は同じ`name`グループ内のいずれかが`checked`であれば充足(HTML5
2779    /// 仕様どおり)。それ以外の`input`/`select`/`textarea`は`value`(trim後)が
2780    /// 空でなければ充足。`validate_field`/`validity_flags`の両方から共通で使う。
2781    fn required_satisfied(&self, n: &DomNode, input_type: &str, trimmed_value: &str) -> bool {
2782        let is_checked = |node: &DomNode| {
2783            if let Some(lc) = node.attrs.get("_live_checked") {
2784                lc == "true"
2785            } else {
2786                node.attrs.contains_key("checked")
2787            }
2788        };
2789        match input_type {
2790            "checkbox" => is_checked(n),
2791            "radio" => {
2792                let name = n.attrs.get("name").cloned().unwrap_or_default();
2793                if name.is_empty() {
2794                    is_checked(n)
2795                } else {
2796                    self.nodes.iter().any(|other| {
2797                        other.tag == "input"
2798                            && other
2799                                .attrs
2800                                .get("type")
2801                                .map(|t| t.eq_ignore_ascii_case("radio"))
2802                                .unwrap_or(false)
2803                            && other.attrs.get("name").map(|nm| nm == &name).unwrap_or(false)
2804                            && is_checked(other)
2805                    })
2806                }
2807            }
2808            _ => !trimmed_value.is_empty(),
2809        }
2810    }
2811
2812    /// HTML5 制約バリデーション。フィールド要素 `idx` と現在値 `value` を検査し、
2813    /// 違反があればユーザー向けメッセージを返す(None = 妥当)。
2814    /// 対応: setCustomValidity(_custom_validity) / required / minlength / maxlength /
2815    /// pattern / type=email / type=url / type=number,range(min,max,step)。
2816    /// disabled/readonly や type=submit/button/hidden は検証対象外。
2817    pub fn validate_field(&self, idx: usize, value: &str) -> Option<String> {
2818        let n = self.nodes.get(idx)?;
2819        if n.is_text {
2820            return None;
2821        }
2822        // 無効・隠し要素はスキップ(`<fieldset disabled>`祖先による暗黙の無効化も
2823        // 含む。詳細は`is_disabled`のコメント参照)。
2824        if self.is_disabled(idx) {
2825            return None;
2826        }
2827        // `validity_flags`で発見・修正したのと同根のバグ:制約検証の対象候補に
2828        // なれるのは`button`/`input`/`select`/`textarea`の4タグのみだが、タグを
2829        // 一切チェックしていなかった。`form.checkValidity()`側は`form_associated_
2830        // controls`で事前に`input`/`textarea`/`select`へ絞り込んでいるため実害が
2831        // 無かったが、`element.checkValidity()`を非候補要素(`<output>`/`<div>`
2832        // 等)へ直接呼ぶ経路はこのガードが無いまま`validate_field`へ落ちており、
2833        // 誤って`required`属性を付けると本来検証対象外のはずが invalid 扱いに
2834        // なる実害のあるバグだった。2026-07-18 発見・修正)。
2835        if !matches!(n.tag.as_str(), "button" | "input" | "select" | "textarea") {
2836            return None;
2837        }
2838        let input_type = n
2839            .attrs
2840            .get("type")
2841            .map(|s| s.trim().to_lowercase())
2842            .unwrap_or_default();
2843        if matches!(
2844            input_type.as_str(),
2845            "submit" | "button" | "reset" | "hidden" | "image"
2846        ) {
2847            return None;
2848        }
2849        // 1) setCustomValidity で設定されたカスタムメッセージが最優先。
2850        if let Some(cv) = n.attrs.get("_custom_validity") {
2851            if !cv.is_empty() {
2852                return Some(cv.clone());
2853            }
2854        }
2855        let trimmed = value.trim();
2856        // 2) required: 空なら違反。ただし checkbox/radio は「値」ではなく
2857        // 「チェック状態」で判定する別のセマンティクス(HTML5仕様: checkbox は
2858        // 自身の checked、radio は同じ name グループ内のいずれかが checked で
2859        // あれば充足)のため `required_satisfied` に委譲する。以前は
2860        // checkbox/radio も他の input と同じ「value 属性が空か」で判定しており、
2861        // `value` 属性を明示しない(=チェック済みでも常に空文字列扱いになる)
2862        // チェックボックスは常に required 違反、逆に `value="yes"` だけ設定して
2863        // 未チェックのチェックボックスは常に妥当、というチェック状態と無関係な
2864        // 結果になっていた。
2865        if n.attrs.contains_key("required") && !self.required_satisfied(n, &input_type, trimmed) {
2866            return Some(match input_type.as_str() {
2867                "checkbox" => String::from("このフィールドをオンにしてください。"),
2868                "radio" => String::from("いずれかを選択してください。"),
2869                _ => String::from("このフィールドを入力してください。"),
2870            });
2871        }
2872        if matches!(input_type.as_str(), "checkbox" | "radio") {
2873            return None;
2874        }
2875        // 空かつ非required なら以降の形式チェックは不要。
2876        if value.is_empty() {
2877            return None;
2878        }
2879        // 3) minlength / maxlength(文字数)。
2880        let char_len = value.chars().count();
2881        if let Some(ml) = n
2882            .attrs
2883            .get("minlength")
2884            .and_then(|v| v.trim().parse::<usize>().ok())
2885        {
2886            if char_len < ml {
2887                return Some(alloc::format!(
2888                    "{}文字以上で入力してください(現在{}文字)。",
2889                    ml,
2890                    char_len
2891                ));
2892            }
2893        }
2894        if let Some(ml) = n
2895            .attrs
2896            .get("maxlength")
2897            .and_then(|v| v.trim().parse::<usize>().ok())
2898        {
2899            if char_len > ml {
2900                return Some(alloc::format!(
2901                    "{}文字以下で入力してください(現在{}文字)。",
2902                    ml,
2903                    char_len
2904                ));
2905            }
2906        }
2907        // 4) type 別の形式チェック。
2908        match input_type.as_str() {
2909            "email" if !Self::is_valid_email_value(n, value) => {
2910                return Some(String::from("メールアドレスを入力してください。"));
2911            }
2912            "url" if !(value.starts_with("http://") || value.starts_with("https://")) => {
2913                return Some(String::from("URL を入力してください。"));
2914            }
2915            // `type="date"`/`type="month"`の形式・min/max範囲チェック(詳細は
2916            // `validity_flags`側の同種コメント参照。既存の`input_value_as_date_ms()`を
2917            // そのまま再利用)。
2918            // `type="datetime-local"`も`input_value_as_date_ms()`が既に
2919            // `YYYY-MM-DDTHH:MM`形式をサポートしているため、`date`/`month`と
2920            // 同じ扱いに含めるだけで対応できる(`week`/`time`はこの関数自体が
2921            // 対応していないため対象外のまま)。
2922            "date" | "month" | "datetime-local" => match super::builtins::input_value_as_date_ms(&input_type, value) {
2923                Some(ms) => {
2924                    if let Some(min) = n
2925                        .attrs
2926                        .get("min")
2927                        .and_then(|v| super::builtins::input_value_as_date_ms(&input_type, v))
2928                    {
2929                        if ms < min {
2930                            return Some(alloc::format!(
2931                                "{}以降の日付を入力してください。",
2932                                n.attrs.get("min").cloned().unwrap_or_default()
2933                            ));
2934                        }
2935                    }
2936                    if let Some(max) = n
2937                        .attrs
2938                        .get("max")
2939                        .and_then(|v| super::builtins::input_value_as_date_ms(&input_type, v))
2940                    {
2941                        if ms > max {
2942                            return Some(alloc::format!(
2943                                "{}以前の日付を入力してください。",
2944                                n.attrs.get("max").cloned().unwrap_or_default()
2945                            ));
2946                        }
2947                    }
2948                }
2949                None => return Some(String::from("正しい日付を入力してください。")),
2950            },
2951            // `type="range"`も`type="number"`と同じ min/max/step 検証規則を適用する
2952            // (詳細は`validity_flags`側の同種コメント参照。以前は`range`型だけ
2953            // 検証が丸ごと欠けていた)。
2954            "number" | "range" => match value.trim().parse::<f64>() {
2955                Ok(num) => {
2956                    if let Some(min) = n
2957                        .attrs
2958                        .get("min")
2959                        .and_then(|v| v.trim().parse::<f64>().ok())
2960                    {
2961                        if num < min {
2962                            return Some(alloc::format!("{}以上の値を入力してください。", min));
2963                        }
2964                    }
2965                    if let Some(max) = n
2966                        .attrs
2967                        .get("max")
2968                        .and_then(|v| v.trim().parse::<f64>().ok())
2969                    {
2970                        if num > max {
2971                            return Some(alloc::format!("{}以下の値を入力してください。", max));
2972                        }
2973                    }
2974                    // `stepMismatch`(HTML5 制約検証。min/max はあったが step が丸ごと
2975                    // 未対応だった)。`step="any"` は制約無し。既定 step は 1、step の
2976                    // 基準点(仕様上の「step base」)は `min` があればそれ、無ければ 0。
2977                    let step_attr = n.attrs.get("step").map(|s| s.trim());
2978                    if step_attr != Some("any") {
2979                        let step = step_attr
2980                            .and_then(|s| s.parse::<f64>().ok())
2981                            .filter(|s| *s > 0.0)
2982                            .unwrap_or(1.0);
2983                        let base = n
2984                            .attrs
2985                            .get("min")
2986                            .and_then(|v| v.trim().parse::<f64>().ok())
2987                            .unwrap_or(0.0);
2988                        let steps = (num - base) / step;
2989                        if libm::fabs(steps - libm::round(steps)) > 1e-9 {
2990                            return Some(alloc::format!(
2991                                "{} の倍数(基準値 {})で入力してください。",
2992                                step,
2993                                base
2994                            ));
2995                        }
2996                    }
2997                }
2998                Err(_) => return Some(String::from("数値を入力してください。")),
2999            },
3000            _ => {}
3001        }
3002        // 5) pattern(簡易: 完全一致のリテラル比較、ワイルドカードのみ対応)。
3003        if let Some(pat) = n.attrs.get("pattern") {
3004            if !pat.is_empty() && !Self::pattern_matches(pat, value) {
3005                return Some(String::from("要求された形式で入力してください。"));
3006            }
3007        }
3008        None
3009    }
3010
3011    /// 簡易メールアドレス検証(完全な RFC ではなく実用的な近似)。
3012    fn is_valid_email(s: &str) -> bool {
3013        let (local, domain) = match s.split_once('@') {
3014            Some(v) => v,
3015            None => return false,
3016        };
3017        if local.is_empty() || domain.is_empty() {
3018            return false;
3019        }
3020        if domain.contains('@') {
3021            return false;
3022        }
3023        // ドメインに '.' があり、前後が非空。
3024        match domain.split_once('.') {
3025            Some((a, b)) => !a.is_empty() && !b.is_empty(),
3026            None => false,
3027        }
3028    }
3029
3030    /// `<input type="email" multiple>`(HTML5。`multiple`属性がある email input は
3031    /// カンマ区切りで複数アドレスを入力できる仕様だが、以前は`is_valid_email(value)`
3032    /// を値全体にそのまま適用しており、2件目以降のアドレスやカンマ自体を含む文字列は
3033    /// 単一アドレスとして常に不正と誤判定されていた(逆に`multiple`が無いのに
3034    /// カンマ区切りを入力した場合はこのメソッドを経由しないため今までどおり単一
3035    /// アドレスとして正しく不正判定される)。空要素(`"a@b.com,"`のような末尾カンマ)
3036    /// は仕様上不正。
3037    fn is_valid_email_value(n: &DomNode, value: &str) -> bool {
3038        if n.attrs.contains_key("multiple") {
3039            value.split(',').all(|part| Self::is_valid_email(part.trim()))
3040        } else {
3041            Self::is_valid_email(value)
3042        }
3043    }
3044
3045    /// `<input pattern="...">`(HTML Living Standard の制約検証。仕様上は値全体を
3046    /// `^(?:pattern)$` として完全一致させる)が、以前はメタ文字を含む複雑なパターンを
3047    /// 「誤検知で送信を妨げないため」全て無条件で通過させる簡易近似だった。この
3048    /// エンジンには既に本物の正規表現実装(`js::regex::Regex`)があるため、それを
3049    /// そのまま使えば近似ではなく正しく検証できる(`\d`/`[...]`/`+`/`.*` 等、
3050    /// HTML pattern で使われる構文は通常の JS RegExp と同じ)。
3051    fn pattern_matches(pat: &str, val: &str) -> bool {
3052        let anchored = format!("^(?:{})$", pat);
3053        let re = super::regex::Regex::new(&anchored, "");
3054        let chars: alloc::vec::Vec<char> = val.chars().collect();
3055        re.find_at(&chars, 0).is_some()
3056    }
3057
3058    /// 属性名の一覧(id/class を先頭に、続いてその他属性)。
3059    pub fn attr_names(&self, idx: usize) -> Vec<String> {
3060        let mut out = Vec::new();
3061        if let Some(n) = self.nodes.get(idx) {
3062            if !n.id.is_empty() {
3063                out.push(String::from("id"));
3064            }
3065            if !n.classes.is_empty() {
3066                out.push(String::from("class"));
3067            }
3068            // id/class は専用フィールドで既に出力済みなので attrs 側では除外。
3069            // `_` 始まりの内部専用属性(`_content_frag` 等)も
3070            // `serialize_node_html` と同じ理由で除外する(以前は
3071            // `getAttributeNames()`/`hasAttributes()` にそのまま漏れていた)。
3072            for k in n.attrs.keys() {
3073                if k != "id" && k != "class" && !k.starts_with('_') {
3074                    out.push(k.clone());
3075                }
3076            }
3077        }
3078        out
3079    }
3080
3081    pub fn add_listener(&mut self, node: usize, event: &str, func: Value) {
3082        self.add_listener_opts(node, event, func, false, false);
3083    }
3084
3085    /// capture / once オプション付きでリスナを登録する。重複(同一 node+event+func+capture)
3086    /// は DOM 仕様どおり無視する。戻り値は発番した ID。
3087    pub fn add_listener_opts(
3088        &mut self,
3089        node: usize,
3090        event: &str,
3091        func: Value,
3092        capture: bool,
3093        once: bool,
3094    ) -> u64 {
3095        self.add_listener_opts_signal(node, event, func, capture, once, None)
3096    }
3097
3098    /// `add_listener_opts` の `{signal}` 対応版。`signal` が既に abort 済みなら
3099    /// 仕様どおり登録自体を行わない。
3100    pub fn add_listener_opts_signal(
3101        &mut self,
3102        node: usize,
3103        event: &str,
3104        func: Value,
3105        capture: bool,
3106        once: bool,
3107        signal: Option<Value>,
3108    ) -> u64 {
3109        if signal.as_ref().is_some_and(is_signal_aborted) {
3110            return 0;
3111        }
3112        // 同一 (node, event, func, capture) の重複登録は無視(DOM 仕様)。
3113        if self.listeners.iter().any(|l| {
3114            l.node == node && l.event == event && l.capture == capture && l.func.same_ref(&func)
3115        }) {
3116            return 0;
3117        }
3118        let id = self.next_listener_id;
3119        self.next_listener_id += 1;
3120        self.listeners.push(Listener {
3121            node,
3122            event: event.to_string(),
3123            func,
3124            capture,
3125            once,
3126            id,
3127            signal,
3128        });
3129        id
3130    }
3131
3132    /// removeEventListener: 同一 (node, event, func, capture) のリスナを削除する。
3133    pub fn remove_listener(&mut self, node: usize, event: &str, func: &Value, capture: bool) {
3134        self.listeners.retain(|l| {
3135            !(l.node == node && l.event == event && l.capture == capture && l.func.same_ref(func))
3136        });
3137    }
3138
3139    /// ID 指定でリスナを削除する(once 発火後の自動削除に使う)。
3140    pub fn remove_listener_by_id(&mut self, id: u64) {
3141        self.listeners.retain(|l| l.id != id);
3142    }
3143
3144    /// 指定イベント型のリスナがページ内に1つでも登録されているか(ホバー判定の省略用)。
3145    pub fn has_listener(&self, event: &str) -> bool {
3146        self.listeners.iter().any(|l| l.event == event)
3147    }
3148
3149    /// 指定ノードに登録された指定イベントのリスナ関数を返す(呼び出しは呼び元で)。
3150    /// バブリングフェーズ用(capture=false のみ)。`{signal}` が abort 済みの
3151    /// リスナは除外する。
3152    pub fn listeners_for(&self, node: usize, event: &str) -> Vec<Value> {
3153        self.listeners
3154            .iter()
3155            .filter(|l| l.node == node && l.event == event && !l.capture && !is_listener_aborted(l))
3156            .map(|l| l.func.clone())
3157            .collect()
3158    }
3159
3160    /// 指定ノード・イベントのリスナを (func, capture, once, id) 付きで返す。
3161    /// `want_capture` でキャプチャ/バブルのどちらのフェーズかを絞り込む。
3162    /// `{signal}` が abort 済みのリスナは除外する。
3163    pub fn listeners_phase(
3164        &self,
3165        node: usize,
3166        event: &str,
3167        want_capture: bool,
3168    ) -> Vec<(Value, bool, u64)> {
3169        self.listeners
3170            .iter()
3171            .filter(|l| l.node == node && l.event == event && l.capture == want_capture && !is_listener_aborted(l))
3172            .map(|l| (l.func.clone(), l.once, l.id))
3173            .collect()
3174    }
3175
3176    /// 指定ノードに当該イベントのリスナ(フェーズ問わず)が在るか。
3177    pub fn has_listener_on(&self, node: usize, event: &str) -> bool {
3178        self.listeners
3179            .iter()
3180            .any(|l| l.node == node && l.event == event)
3181    }
3182
3183    /// 再パースした DOM 木へ JS の変更を焼き戻す。木はビルド時と同一ドキュメント順前提。
3184    pub fn apply_overrides(&self, root: &mut Node) {
3185        let mut counter = 0usize;
3186        self.apply_node(root, &mut counter);
3187    }
3188    fn apply_node(&self, node: &mut Node, counter: &mut usize) {
3189        let idx = *counter;
3190        *counter += 1;
3191        if let Some(dn) = self.nodes.get(idx) {
3192            match &mut node.node_type {
3193                NodeType::Text(t) => {
3194                    if let Some(ov) = &dn.text_override {
3195                        *t = ov.clone();
3196                    }
3197                }
3198                NodeType::Element {
3199                    attributes,
3200                    classes,
3201                    ..
3202                } => {
3203                    if dn.classes_dirty {
3204                        *classes = dn.classes.clone();
3205                        attributes.insert("class".to_string(), dn.classes.join(" "));
3206                    }
3207                    if dn.style_dirty {
3208                        attributes.insert("style".to_string(), serialize_inline_style(&dn.style));
3209                    }
3210                }
3211            }
3212        }
3213        for c in &mut node.children {
3214            self.apply_node(c, counter);
3215        }
3216    }
3217}
3218
3219/// `outerHTML` 直列化用の最小限の HTML エスケープ(テキストノード本文)。
3220fn html_escape_text(s: &str) -> String {
3221    s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")
3222}
3223/// `outerHTML` 直列化用の最小限の HTML エスケープ(属性値。`"` も escape する)。
3224fn html_escape_attr(s: &str) -> String {
3225    s.replace('&', "&amp;").replace('"', "&quot;")
3226}
3227
3228/// 生の宣言値末尾の `!important`(大小文字・空白ゆれ許容)を切り離す。
3229/// `(値本体, important か)` を返す。CSS カスケード側(`css.rs`)の
3230/// `!important` パーサとは別実装だが、`element.style.getPropertyValue`/
3231/// `.getPropertyPriority`(丸ごと未対応だった。2026-07-16 発見)専用の
3232/// 軽量版。
3233fn strip_style_priority(raw: &str) -> (&str, bool) {
3234    let trimmed = raw.trim_end();
3235    if let Some(pos) = trimmed.to_ascii_lowercase().rfind("!important") {
3236        if trimmed.get(pos..).unwrap_or("").eq_ignore_ascii_case("!important") {
3237            let before = trimmed.get(..pos).unwrap_or("").trim_end();
3238            return (before, true);
3239        }
3240    }
3241    (raw, false)
3242}
3243
3244/// `color: red; font-size: 12px` → マップ。
3245fn parse_inline_style(s: &str) -> BTreeMap<String, String> {
3246    let mut m = BTreeMap::new();
3247    for decl in s.split(';') {
3248        let decl = decl.trim();
3249        if decl.is_empty() {
3250            continue;
3251        }
3252        if let Some(colon) = decl.find(':') {
3253            let k = decl.get(..colon).unwrap_or("").trim().to_lowercase();
3254            let v = decl.get(colon + 1..).unwrap_or("").trim().to_string();
3255            if !k.is_empty() {
3256                m.insert(k, v);
3257            }
3258        }
3259    }
3260    m
3261}
3262
3263fn serialize_inline_style(m: &BTreeMap<String, String>) -> String {
3264    let mut parts = Vec::new();
3265    for (k, v) in m {
3266        parts.push(format!("{}: {}", k, v));
3267    }
3268    parts.join("; ")
3269}
3270
3271/// `backgroundColor` → `background-color`。既にケバブならそのまま。
3272fn css_prop_from_camel(prop: &str) -> String {
3273    if prop.contains('-') || !prop.chars().any(|c| c.is_ascii_uppercase()) {
3274        return prop.to_lowercase();
3275    }
3276    let mut out = String::new();
3277    for c in prop.chars() {
3278        if c.is_ascii_uppercase() {
3279            out.push('-');
3280            out.push(c.to_ascii_lowercase());
3281        } else {
3282            out.push(c);
3283        }
3284    }
3285    out
3286}