Skip to main content

atmos/os_lib/js/builtins/
dom_forward.rs

1//! `Node.prototype`・`Element.prototype` に置く、組込みの DOM メソッドへの転送。
2//!
3//! 【2026-10-20】DOM 要素のメソッドは実体(ハンドル)から動的に解決しており、原型には
4//! 無かった。カスタム要素のクラスが `super.removeAttribute(…)`(ES5 版では
5//! `M.prototype.removeAttribute.call(this, …)`)で組込みを呼ぶ形や、ShadyDOM が
6//! `Element.prototype` の記述子を写し取る形が成り立たなかった(YouTube の Polymer)。
7//! ここに置く関数は、`this` の要素の組込みメソッドを引いて呼ぶだけ。
8
9use super::*;
10
11/// `this` の組込みメンバ `name`(DOM 要素、または document 等のホストオブジェクト)。
12fn builtin_member(it: &mut Interp, this: &Value, name: &str) -> Result<Value, Value> {
13    if let Some(idx) = this_dom_idx(this) {
14        return Ok(it.dom_builtin_get(idx, name));
15    }
16    let host = match this {
17        Value::Object(o) => match &o.borrow().kind {
18            ObjKind::Host(tag) => Some(tag.clone()),
19            _ => None,
20        },
21        _ => None,
22    };
23    match host {
24        Some(tag) => Ok(it.dom_get_property(crate::os_lib::js::interp::DomDisp::Host(tag), name)),
25        None => Err(it.type_error(alloc::format!("Illegal invocation: {}", name))),
26    }
27}
28
29/// `this`(DOM 要素・ホストオブジェクト)の組込みメソッド `name` を呼ぶ。
30fn forward(it: &mut Interp, this: Value, a: &[Value], name: &str) -> Result<Value, Value> {
31    let f = builtin_member(it, &this, name)?;
32    if !matches!(&f, Value::Object(o) if o.borrow().is_callable()) {
33        return Err(it.type_error(alloc::format!("{} is not a function", name)));
34    }
35    it.call_value(&f, this, a)
36}
37
38macro_rules! forwarders {
39    ($($fname:ident => $key:literal),* $(,)?) => {
40        $(
41            fn $fname(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
42                forward(it, this, a, $key)
43            }
44        )*
45        /// (名前, 関数) の一覧。
46        const FORWARDS: &[(&str, NativeFn)] = &[$(($key, $fname)),*];
47    };
48}
49
50forwarders! {
51    f_append_child => "appendChild",
52    f_insert_before => "insertBefore",
53    f_remove_child => "removeChild",
54    f_replace_child => "replaceChild",
55    f_clone_node => "cloneNode",
56    f_contains => "contains",
57    f_has_child_nodes => "hasChildNodes",
58    f_get_root_node => "getRootNode",
59    f_is_equal_node => "isEqualNode",
60    f_compare_document_position => "compareDocumentPosition",
61    f_normalize => "normalize",
62    f_get_attribute => "getAttribute",
63    f_set_attribute => "setAttribute",
64    f_remove_attribute => "removeAttribute",
65    f_has_attribute => "hasAttribute",
66    f_toggle_attribute => "toggleAttribute",
67    f_get_attribute_names => "getAttributeNames",
68    f_has_attributes => "hasAttributes",
69    f_query_selector => "querySelector",
70    f_query_selector_all => "querySelectorAll",
71    f_get_elements_by_tag_name => "getElementsByTagName",
72    f_get_elements_by_class_name => "getElementsByClassName",
73    f_closest => "closest",
74    f_matches => "matches",
75    f_get_bounding_client_rect => "getBoundingClientRect",
76    f_get_client_rects => "getClientRects",
77    f_attach_shadow => "attachShadow",
78    f_insert_adjacent_html => "insertAdjacentHTML",
79    f_insert_adjacent_element => "insertAdjacentElement",
80    f_insert_adjacent_text => "insertAdjacentText",
81    f_remove => "remove",
82    f_append => "append",
83    f_prepend => "prepend",
84    f_after => "after",
85    f_before => "before",
86    f_replace_with => "replaceWith",
87    f_replace_children => "replaceChildren",
88    f_scroll_into_view => "scrollIntoView",
89    f_focus => "focus",
90    f_blur => "blur",
91    f_click => "click",
92}
93
94/// `this`(DOM 要素)の組込みのプロパティ `name` を読む。
95fn forward_get(it: &mut Interp, this: Value, name: &str) -> Result<Value, Value> {
96    builtin_member(it, &this, name)
97}
98
99/// `this`(DOM 要素)の組込みのプロパティ `name` に書く。
100fn forward_set(it: &mut Interp, this: Value, a: &[Value], name: &str) -> Result<Value, Value> {
101    if let Some(idx) = this_dom_idx(&this) {
102        it.dom_builtin_set(idx, name, arg(a, 0));
103        return Ok(Value::Undefined);
104    }
105    let host = match &this {
106        Value::Object(o) => match &o.borrow().kind {
107            ObjKind::Host(tag) => Some(tag.clone()),
108            _ => None,
109        },
110        _ => None,
111    };
112    match host {
113        Some(tag) => {
114            it.dom_set_property(crate::os_lib::js::interp::DomDisp::Host(tag), name, arg(a, 0));
115            Ok(Value::Undefined)
116        }
117        None => Err(it.type_error(alloc::format!("Illegal invocation: {}", name))),
118    }
119}
120
121macro_rules! forwarders_doc {
122    ($($fname:ident => $key:literal),* $(,)?) => {
123        $(
124            fn $fname(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
125                forward(it, this, a, $key)
126            }
127        )*
128        /// `Document.prototype` に置く (名前, 関数) の一覧。
129        const DOC_FORWARDS: &[(&str, NativeFn)] = &[$(($key, $fname)),*];
130    };
131}
132
133macro_rules! accessors {
134    ($($node:literal $gname:ident $sname:ident => $key:literal $writable:literal),* $(,)?) => {
135        $(
136            fn $gname(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
137                forward_get(it, this, $key)
138            }
139            fn $sname(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
140                forward_set(it, this, a, $key)
141            }
142        )*
143        /// (Node.prototype に置くか, 名前, getter, setter, 書けるか) の一覧。
144        const ACCESSORS: &[(bool, &str, NativeFn, NativeFn, bool)] = &[$(($node, $key, $gname, $sname, $writable)),*];
145    };
146}
147
148// 【2026-10-20】ShadyDOM(noPatch)は `Node.prototype`/`Element.prototype` のアクセサの記述子を
149// `__shady_native_*` に写して使う。無いと `ShadyDOM.wrap(el).isConnected` 等が undefined になり、
150// YouTube の ytd-app が disable-upgrade の解除後に connectedCallback を呼ばなかった。
151accessors! {
152    true g_parent_node s_parent_node => "parentNode" false,
153    true g_parent_element s_parent_element => "parentElement" false,
154    true g_first_child s_first_child => "firstChild" false,
155    true g_last_child s_last_child => "lastChild" false,
156    true g_previous_sibling s_previous_sibling => "previousSibling" false,
157    true g_next_sibling s_next_sibling => "nextSibling" false,
158    true g_child_nodes s_child_nodes => "childNodes" false,
159    true g_is_connected s_is_connected => "isConnected" false,
160    true g_owner_document s_owner_document => "ownerDocument" false,
161    true g_node_type s_node_type => "nodeType" false,
162    true g_node_name s_node_name => "nodeName" false,
163    true g_text_content s_text_content => "textContent" true,
164    false g_children s_children => "children" false,
165    false g_first_element_child s_first_element_child => "firstElementChild" false,
166    false g_last_element_child s_last_element_child => "lastElementChild" false,
167    false g_previous_element_sibling s_previous_element_sibling => "previousElementSibling" false,
168    false g_next_element_sibling s_next_element_sibling => "nextElementSibling" false,
169    false g_child_element_count s_child_element_count => "childElementCount" false,
170    false g_inner_html s_inner_html => "innerHTML" true,
171    false g_outer_html s_outer_html => "outerHTML" true,
172    false g_class_name s_class_name => "className" true,
173    false g_id s_id => "id" true,
174    false g_slot s_slot => "slot" true,
175    false g_shadow_root s_shadow_root => "shadowRoot" false,
176    false g_assigned_slot s_assigned_slot => "assignedSlot" false,
177    false g_tag_name s_tag_name => "tagName" false,
178    false g_local_name s_local_name => "localName" false,
179}
180
181forwarders_doc! {
182    d_create_element => "createElement",
183    d_create_element_ns => "createElementNS",
184    d_create_text_node => "createTextNode",
185    d_create_comment => "createComment",
186    d_create_document_fragment => "createDocumentFragment",
187    d_import_node => "importNode",
188    d_adopt_node => "adoptNode",
189    d_get_element_by_id => "getElementById",
190    d_create_tree_walker => "createTreeWalker",
191    d_create_event => "createEvent",
192    d_create_range => "createRange",
193    d_element_from_point => "elementFromPoint",
194}
195
196/// Node 系のメソッド名(`Node.prototype` に置く。残りは `Element.prototype`)。
197const NODE_METHODS: &[&str] = &[
198    "appendChild",
199    "insertBefore",
200    "removeChild",
201    "replaceChild",
202    "cloneNode",
203    "contains",
204    "hasChildNodes",
205    "getRootNode",
206    "isEqualNode",
207    "compareDocumentPosition",
208    "normalize",
209];
210
211/// `Node.prototype` と `Element.prototype` に転送用の関数を置く。
212pub(crate) fn install(node_proto: &ObjRef, element_proto: &ObjRef) {
213    for (name, f) in FORWARDS {
214        let target = if NODE_METHODS.contains(name) { node_proto } else { element_proto };
215        let mut b = target.borrow_mut();
216        if !b.props.contains_key(*name) {
217            b.props.insert(String::from(*name), nv(name, *f));
218        }
219    }
220    for (on_node, name, g, set, writable) in ACCESSORS {
221        let target = if *on_node { node_proto } else { element_proto };
222        let mut b = target.borrow_mut();
223        if !b.accessors.contains_key(name) && !b.props.contains_key(*name) {
224            b.accessors.insert(
225                String::from(*name),
226                Accessor { get: Some(nv(name, *g)), set: if *writable { Some(nv(name, *set)) } else { None } },
227            );
228        }
229    }
230}
231
232/// `attributes.getNamedItem(name)`(属性の配列に名前で付けたものを引く。無ければ null)。
233pub(crate) fn named_node_map_get(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
234    let name = arg(a, 0).to_js_string();
235    if let Value::Object(o) = &this {
236        if let Some(v @ Value::Object(_)) = o.borrow().props.get(&name) {
237            return Ok(v.clone());
238        }
239    }
240    Ok(Value::Null)
241}
242
243/// `attributes.item(i)`(範囲外は null)。
244pub(crate) fn named_node_map_item(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
245    let i = arg(a, 0).to_number();
246    if let Value::Object(o) = &this {
247        if let ObjKind::Array(items) = &o.borrow().kind {
248            if i >= 0.0 {
249                if let Some(v) = items.get(i as usize) {
250                    return Ok(v.clone());
251                }
252            }
253        }
254    }
255    Ok(Value::Null)
256}
257
258/// `Document.prototype` に document の組込みメソッドへの転送を置く(ShadyDOM が写す)。
259pub(crate) fn install_document(doc_proto: &ObjRef) {
260    let mut b = doc_proto.borrow_mut();
261    for (name, f) in DOC_FORWARDS {
262        if !b.props.contains_key(*name) {
263            b.props.insert(String::from(*name), nv(name, *f));
264        }
265    }
266}
267
268/// `DocumentFragment.prototype` に置く要素検索・追加の転送(YouTube は
269/// `DocumentFragment.prototype.querySelectorAll.call(frag, "*")` を使う)。
270pub(crate) fn install_fragment(df_proto: &ObjRef) {
271    const NAMES: &[&str] = &["querySelector", "querySelectorAll", "append", "prepend", "replaceChildren"];
272    let mut b = df_proto.borrow_mut();
273    for (name, f) in FORWARDS {
274        if NAMES.contains(name) && !b.props.contains_key(*name) {
275            b.props.insert(String::from(*name), nv(name, *f));
276        }
277    }
278    for (_, name, g, _, _) in ACCESSORS {
279        if matches!(*name, "children" | "firstElementChild" | "lastElementChild" | "childElementCount") && !b.accessors.contains_key(name) {
280            b.accessors.insert(String::from(*name), Accessor { get: Some(nv(name, *g)), set: None });
281        }
282    }
283}