Skip to main content

script/dom/
customelementregistry.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::collections::VecDeque;
7use std::ffi::CStr;
8use std::ptr::NonNull;
9use std::rc::Rc;
10use std::{mem, ptr};
11
12use dom_struct::dom_struct;
13use html5ever::{LocalName, Namespace, Prefix, ns};
14use js::context::JSContext;
15use js::conversions::FromJSValConvertible;
16use js::glue::UnwrapObjectStatic;
17use js::jsapi::{HandleValueArray, Heap, IsCallable, IsConstructor, JSAutoRealm, JSObject};
18use js::jsval::{BooleanValue, JSVal, NullValue, ObjectValue, UndefinedValue};
19use js::realm::{AutoRealm, CurrentRealm};
20use js::rust::wrappers2::{Construct1, JS_GetProperty, SameValue};
21use js::rust::{HandleObject, MutableHandleValue};
22use rustc_hash::FxBuildHasher;
23use script_bindings::cell::DomRefCell;
24use script_bindings::conversions::SafeToJSValConvertible;
25use script_bindings::reflector::{DomObject, Reflector, reflect_dom_object};
26use script_bindings::settings_stack::{run_a_callback, run_a_script};
27use style::attr::AttrValue;
28
29use super::bindings::trace::HashMapTracedValues;
30use crate::DomTypeHolder;
31use crate::dom::bindings::callback::{CallbackContainer, ExceptionHandling};
32use crate::dom::bindings::codegen::Bindings::CustomElementRegistryBinding::{
33    CustomElementConstructor, CustomElementRegistryMethods, ElementDefinitionOptions,
34};
35use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods;
36use crate::dom::bindings::codegen::Bindings::FunctionBinding::Function;
37use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
38use crate::dom::bindings::conversions::{ConversionResult, StringificationBehavior, get_property};
39use crate::dom::bindings::error::{
40    Error, ErrorResult, Fallible, report_pending_exception, throw_dom_exception,
41};
42use crate::dom::bindings::inheritance::{Castable, NodeTypeId};
43use crate::dom::bindings::reflector::DomGlobal;
44use crate::dom::bindings::root::{AsHandleValue, Dom, DomRoot};
45use crate::dom::bindings::str::DOMString;
46use crate::dom::document::Document;
47use crate::dom::domexception::{DOMErrorName, DOMException};
48use crate::dom::element::Element;
49use crate::dom::globalscope::GlobalScope;
50use crate::dom::html::htmlelement::HTMLElement;
51use crate::dom::html::htmlformelement::{FormControl, HTMLFormElement};
52use crate::dom::iterators::ShadowIncluding;
53use crate::dom::node::{Node, NodeTraits};
54use crate::dom::promise::Promise;
55use crate::dom::window::Window;
56use crate::microtask::Microtask;
57use crate::realms::enter_auto_realm;
58use crate::script_runtime::CanGc;
59use crate::script_thread::ScriptThread;
60
61/// <https://dom.spec.whatwg.org/#concept-element-custom-element-state>
62#[derive(Clone, Copy, Default, Eq, JSTraceable, MallocSizeOf, PartialEq)]
63pub(crate) enum CustomElementState {
64    Undefined,
65    Failed,
66    #[default]
67    Uncustomized,
68    Precustomized,
69    Custom,
70}
71
72/// <https://html.spec.whatwg.org/multipage/#customelementregistry>
73#[dom_struct]
74pub(crate) struct CustomElementRegistry {
75    reflector_: Reflector,
76
77    window: Dom<Window>,
78
79    #[conditional_malloc_size_of]
80    /// It is safe to use FxBuildHasher here as `LocalName` is an `Atom` in the string_cache.
81    /// These get a u32 hashed instead of a string.
82    when_defined: DomRefCell<HashMapTracedValues<LocalName, Rc<Promise>, FxBuildHasher>>,
83
84    element_definition_is_running: Cell<bool>,
85
86    #[conditional_malloc_size_of]
87    definitions:
88        DomRefCell<HashMapTracedValues<LocalName, Rc<CustomElementDefinition>, FxBuildHasher>>,
89}
90
91impl CustomElementRegistry {
92    fn new_inherited(window: &Window) -> CustomElementRegistry {
93        CustomElementRegistry {
94            reflector_: Reflector::new(),
95            window: Dom::from_ref(window),
96            when_defined: DomRefCell::new(HashMapTracedValues::new_fx()),
97            element_definition_is_running: Cell::new(false),
98            definitions: DomRefCell::new(HashMapTracedValues::new_fx()),
99        }
100    }
101
102    pub(crate) fn new(window: &Window, can_gc: CanGc) -> DomRoot<CustomElementRegistry> {
103        reflect_dom_object(
104            Box::new(CustomElementRegistry::new_inherited(window)),
105            window,
106            can_gc,
107        )
108    }
109
110    /// Cleans up any active promises
111    /// <https://github.com/servo/servo/issues/15318>
112    pub(crate) fn teardown(&self) {
113        self.when_defined.borrow_mut().0.clear()
114    }
115
116    /// <https://html.spec.whatwg.org/multipage/#look-up-a-custom-element-definition>
117    pub(crate) fn lookup_definition(
118        &self,
119        local_name: &LocalName,
120        is: Option<&LocalName>,
121    ) -> Option<Rc<CustomElementDefinition>> {
122        self.definitions
123            .borrow()
124            .0
125            .values()
126            .find(|definition| {
127                // Step 4-5
128                definition.local_name == *local_name &&
129                    (definition.name == *local_name || Some(&definition.name) == is)
130            })
131            .cloned()
132    }
133
134    pub(crate) fn lookup_definition_by_constructor(
135        &self,
136        constructor: HandleObject,
137    ) -> Option<Rc<CustomElementDefinition>> {
138        self.definitions
139            .borrow()
140            .0
141            .values()
142            .find(|definition| definition.constructor.callback() == constructor.get())
143            .cloned()
144    }
145
146    /// <https://html.spec.whatwg.org/multipage/#look-up-a-custom-element-registry>
147    pub(crate) fn lookup_a_custom_element_registry(
148        node: &Node,
149    ) -> Option<DomRoot<CustomElementRegistry>> {
150        match node.type_id() {
151            // Step 1. If node is an Element object, then return node's custom element registry.
152            NodeTypeId::Element(_) => node
153                .downcast::<Element>()
154                .expect("Nodes with element type must be an element")
155                .custom_element_registry(),
156            // Step 2. If node is a ShadowRoot object, then return node's custom element registry.
157            // TODO
158            // Step 3. If node is a Document object, then return node's custom element registry.
159            NodeTypeId::Document(_) => Some(
160                node.downcast::<Document>()
161                    .expect("Nodes with document type must be a document")
162                    .custom_element_registry(),
163            ),
164            // Step 4. Return null.
165            _ => None,
166        }
167    }
168
169    /// <https://dom.spec.whatwg.org/#is-a-global-custom-element-registry>
170    pub(crate) fn is_a_global_element_registry(registry: Option<&CustomElementRegistry>) -> bool {
171        // Null or a CustomElementRegistry object registry is a global custom element registry
172        // if registry is non-null and registry’s is scoped is false.
173        // TODO: Implement scoped
174        registry.is_some()
175    }
176
177    /// <https://html.spec.whatwg.org/multipage/#dom-customelementregistry-define>
178    /// Steps 10.1, 10.2
179    #[expect(unsafe_code)]
180    fn check_prototype(
181        &self,
182        cx: &mut JSContext,
183        constructor: HandleObject,
184        mut prototype: MutableHandleValue,
185    ) -> ErrorResult {
186        unsafe {
187            // Step 10.1
188            if !JS_GetProperty(cx, constructor, c"prototype".as_ptr(), prototype.reborrow()) {
189                return Err(Error::JSFailed);
190            }
191
192            // Step 10.2
193            if !prototype.is_object() {
194                return Err(Error::Type(
195                    c"constructor.prototype is not an object".to_owned(),
196                ));
197            }
198        }
199        Ok(())
200    }
201
202    /// <https://html.spec.whatwg.org/multipage/#dom-customelementregistry-define>
203    /// This function includes both steps 14.3 and 14.4 which add the callbacks to a map and
204    /// process them.
205    fn get_callbacks(
206        &self,
207        cx: &mut JSContext,
208        prototype: HandleObject,
209    ) -> Fallible<LifecycleCallbacks> {
210        // Step 4
211        Ok(LifecycleCallbacks {
212            connected_callback: get_callback(cx, prototype, c"connectedCallback")?,
213            disconnected_callback: get_callback(cx, prototype, c"disconnectedCallback")?,
214            connected_move_callback: get_callback(cx, prototype, c"connectedMoveCallback")?,
215            adopted_callback: get_callback(cx, prototype, c"adoptedCallback")?,
216            attribute_changed_callback: get_callback(cx, prototype, c"attributeChangedCallback")?,
217
218            form_associated_callback: None,
219            form_disabled_callback: None,
220            form_reset_callback: None,
221            form_state_restore_callback: None,
222        })
223    }
224
225    /// <https://html.spec.whatwg.org/multipage/#dom-customelementregistry-define>
226    /// Step 14.13: Add form associated callbacks to LifecycleCallbacks
227    #[expect(unsafe_code)]
228    unsafe fn add_form_associated_callbacks(
229        &self,
230        cx: &mut JSContext,
231        prototype: HandleObject,
232        callbacks: &mut LifecycleCallbacks,
233    ) -> ErrorResult {
234        callbacks.form_associated_callback =
235            get_callback(cx, prototype, c"formAssociatedCallback")?;
236        callbacks.form_reset_callback = get_callback(cx, prototype, c"formResetCallback")?;
237        callbacks.form_disabled_callback = get_callback(cx, prototype, c"formDisabledCallback")?;
238        callbacks.form_state_restore_callback =
239            get_callback(cx, prototype, c"formStateRestoreCallback")?;
240
241        Ok(())
242    }
243}
244
245/// <https://html.spec.whatwg.org/multipage/#dom-customelementregistry-define>
246/// Step 14.4: Get `callbackValue` for all `callbackName` in `lifecycleCallbacks`.
247#[expect(unsafe_code)]
248fn get_callback(
249    cx: &mut JSContext,
250    prototype: HandleObject,
251    name: &CStr,
252) -> Fallible<Option<Rc<Function>>> {
253    rooted!(&in(cx) let mut callback = UndefinedValue());
254    unsafe {
255        // Step 10.4.1
256        if !JS_GetProperty(cx, prototype, name.as_ptr(), callback.handle_mut()) {
257            return Err(Error::JSFailed);
258        }
259
260        // Step 10.4.2
261        if !callback.is_undefined() {
262            if !callback.is_object() || !IsCallable(callback.to_object()) {
263                return Err(Error::Type(
264                    c"Lifecycle callback is not callable".to_owned(),
265                ));
266            }
267            Ok(Some(Function::new(cx.into(), callback.to_object())))
268        } else {
269            Ok(None)
270        }
271    }
272}
273
274impl CustomElementRegistryMethods<crate::DomTypeHolder> for CustomElementRegistry {
275    #[expect(unsafe_code)]
276    /// <https://html.spec.whatwg.org/multipage/#dom-customelementregistry-define>
277    fn Define(
278        &self,
279        cx: &mut JSContext,
280        name: DOMString,
281        constructor_: Rc<CustomElementConstructor>,
282        options: &ElementDefinitionOptions,
283    ) -> ErrorResult {
284        rooted!(&in(cx) let constructor = constructor_.callback());
285        let name = LocalName::from(name);
286
287        // Step 1. If IsConstructor(constructor) is false, then throw a TypeError.
288        // We must unwrap the constructor as all wrappers are constructable if they are callable.
289        rooted!(&in(cx) let unwrapped_constructor = unsafe { UnwrapObjectStatic(constructor.get()) });
290
291        if unwrapped_constructor.is_null() {
292            // We do not have permission to access the unwrapped constructor.
293            return Err(Error::Security(None));
294        }
295
296        if unsafe { !IsConstructor(unwrapped_constructor.get()) } {
297            return Err(Error::Type(
298                c"Second argument of CustomElementRegistry.define is not a constructor".to_owned(),
299            ));
300        }
301
302        // Step 2. If name is not a valid custom element name, then throw a "SyntaxError" DOMException.
303        if !is_valid_custom_element_name(&name) {
304            return Err(Error::Syntax(Some(format!(
305                "{} name is not a valid custom element name",
306                name
307            ))));
308        }
309
310        // Step 3. If this's custom element definition set contains an item with name name,
311        // then throw a "NotSupportedError" DOMException.
312        if self.definitions.borrow().contains_key(&name) {
313            return Err(Error::NotSupported(Some(format!(
314                "{} has already been defined as a custom element",
315                name
316            ))));
317        }
318
319        // Step 4. If this's custom element definition set contains an
320        // item with constructor constructor, then throw a "NotSupportedError" DOMException.
321        if self
322            .definitions
323            .borrow()
324            .iter()
325            .any(|(_, def)| def.constructor == constructor_)
326        {
327            return Err(Error::NotSupported(None));
328        }
329
330        // Step 6. Let extends be options["extends"] if it exists; otherwise null.
331        let extends = &options.extends;
332
333        // Steps 5, 7
334        let local_name = if let Some(ref extended_name) = *extends {
335            // TODO Step 7.1 If this's is scoped is true, then throw a "NotSupportedError" DOMException.
336
337            // Step 7.2 If extends is a valid custom element name, then throw a "NotSupportedError" DOMException.
338            if is_valid_custom_element_name(&extended_name.str()) {
339                return Err(Error::NotSupported(None));
340            }
341
342            // Step 7.3 If the element interface for extends and the HTML namespace is HTMLUnknownElement
343            // (e.g., if extends does not indicate an element definition in this specification)
344            // then throw a "NotSupportedError" DOMException.
345            if !is_extendable_element_interface(&extended_name.str()) {
346                return Err(Error::NotSupported(None));
347            }
348
349            // Step 7.4 Set localName to extends.
350            LocalName::from(extended_name)
351        } else {
352            // Step 5. Let localName be name.
353            name.clone()
354        };
355
356        // Step 8
357        if self.element_definition_is_running.get() {
358            return Err(Error::NotSupported(None));
359        }
360
361        // Step 9
362        self.element_definition_is_running.set(true);
363
364        // Steps 10-13: Initialize `formAssociated`, `disableInternals`, `disableShadow`, and
365        // `observedAttributes` with default values, but this is done later.
366
367        // Steps 14.1 - 14.2: Get the value of the prototype.
368        rooted!(&in(cx) let mut prototype = UndefinedValue());
369        {
370            let mut realm = AutoRealm::new_from_handle(cx, constructor.handle());
371            if let Err(error) =
372                self.check_prototype(&mut realm, constructor.handle(), prototype.handle_mut())
373            {
374                self.element_definition_is_running.set(false);
375                return Err(error);
376            }
377        };
378
379        // Steps 10.3 - 10.4
380        // It would be easier to get all the callbacks in one pass after
381        // we know whether this definition is going to be form-associated,
382        // but the order of operations is specified and it's observable
383        // if one of the callback getters throws an exception.
384        rooted!(&in(cx) let proto_object = prototype.to_object());
385        let mut callbacks = {
386            let mut realm = AutoRealm::new_from_handle(cx, proto_object.handle());
387            match self.get_callbacks(&mut realm, proto_object.handle()) {
388                Ok(callbacks) => callbacks,
389                Err(error) => {
390                    self.element_definition_is_running.set(false);
391                    return Err(error);
392                },
393            }
394        };
395
396        // Step 14.5: Handle the case where with `attributeChangedCallback` on `lifecycleCallbacks`
397        // is not null.
398        let observed_attributes: Vec<DOMString> = if callbacks.attribute_changed_callback.is_some()
399        {
400            let mut realm = AutoRealm::new_from_handle(cx, constructor.handle());
401            match get_property(
402                &mut realm,
403                constructor.handle(),
404                c"observedAttributes",
405                StringificationBehavior::Default,
406            ) {
407                Ok(attributes) => attributes.unwrap_or_default(),
408                Err(error) => {
409                    self.element_definition_is_running.set(false);
410                    return Err(error);
411                },
412            }
413        } else {
414            Vec::new()
415        };
416
417        // Steps 14.6 - 14.10: Handle `disabledFeatures`.
418        let (disable_internals, disable_shadow) = {
419            let mut realm = AutoRealm::new_from_handle(cx, constructor.handle());
420            match get_property::<Vec<DOMString>>(
421                &mut realm,
422                constructor.handle(),
423                c"disabledFeatures",
424                StringificationBehavior::Default,
425            ) {
426                Ok(sequence) => {
427                    let sequence = sequence.unwrap_or_default();
428                    (
429                        sequence.iter().any(|s| *s == "internals"),
430                        sequence.iter().any(|s| *s == "shadow"),
431                    )
432                },
433                Err(error) => {
434                    self.element_definition_is_running.set(false);
435                    return Err(error);
436                },
437            }
438        };
439
440        // Step 14.11 - 14.12: Handle `formAssociated`.
441        let form_associated: bool = {
442            let mut realm = AutoRealm::new_from_handle(cx, constructor.handle());
443            match get_property(&mut realm, constructor.handle(), c"formAssociated", ()) {
444                Ok(flag) => flag.unwrap_or_default(),
445                Err(error) => {
446                    self.element_definition_is_running.set(false);
447                    return Err(error);
448                },
449            }
450        };
451
452        // Steps 14.13: Add the `formAssociated` callbacks.
453        if form_associated {
454            let mut realm = AutoRealm::new_from_handle(cx, proto_object.handle());
455            unsafe {
456                if let Err(error) = self.add_form_associated_callbacks(
457                    &mut realm,
458                    proto_object.handle(),
459                    &mut callbacks,
460                ) {
461                    self.element_definition_is_running.set(false);
462                    return Err(error);
463                }
464            }
465        }
466
467        self.element_definition_is_running.set(false);
468
469        // Step 15: Set up the new custom element definition.
470        let definition = Rc::new(CustomElementDefinition::new(
471            name.clone(),
472            local_name.clone(),
473            constructor_,
474            observed_attributes,
475            callbacks,
476            form_associated,
477            disable_internals,
478            disable_shadow,
479        ));
480
481        // Step 16: Add definition to this CustomElementRegistry.
482        self.definitions
483            .borrow_mut()
484            .insert(name.clone(), definition.clone());
485
486        // Step 17: Let document be this CustomElementRegistry's relevant global object's
487        // associated Document.
488        let document = self.window.Document();
489
490        // Steps 18-19: Enqueue custom elements upgrade reaction for upgrade candidates.
491        for candidate in document
492            .upcast::<Node>()
493            .traverse_preorder(ShadowIncluding::Yes)
494            .filter_map(DomRoot::downcast::<Element>)
495        {
496            let is = candidate.get_is();
497            if *candidate.local_name() == local_name &&
498                *candidate.namespace() == ns!(html) &&
499                (extends.is_none() || is.as_ref() == Some(&name))
500            {
501                ScriptThread::enqueue_upgrade_reaction(&candidate, definition.clone());
502            }
503        }
504
505        // Step 16, 16.3
506        let promise = self.when_defined.borrow_mut().remove(&name);
507        if let Some(promise) = promise {
508            rooted!(&in(cx) let mut constructor = UndefinedValue());
509            definition.constructor.safe_to_jsval(
510                cx.into(),
511                constructor.handle_mut(),
512                CanGc::from_cx(cx),
513            );
514            promise.resolve_native_with_cx(cx, &constructor.get());
515        }
516        Ok(())
517    }
518
519    /// <https://html.spec.whatwg.org/multipage/#dom-customelementregistry-get>
520    fn Get(&self, cx: &mut JSContext, name: DOMString, mut retval: MutableHandleValue) {
521        match self.definitions.borrow().get(&LocalName::from(name)) {
522            Some(definition) => {
523                definition
524                    .constructor
525                    .safe_to_jsval(cx.into(), retval, CanGc::from_cx(cx))
526            },
527            None => retval.set(UndefinedValue()),
528        }
529    }
530
531    /// <https://html.spec.whatwg.org/multipage/#dom-customelementregistry-getname>
532    fn GetName(&self, constructor: Rc<CustomElementConstructor>) -> Option<DOMString> {
533        self.definitions
534            .borrow()
535            .0
536            .values()
537            .find(|definition| definition.constructor == constructor)
538            .map(|definition| DOMString::from(definition.name.to_string()))
539    }
540
541    /// <https://html.spec.whatwg.org/multipage/#dom-customelementregistry-whendefined>
542    fn WhenDefined(&self, realm: &mut CurrentRealm, name: DOMString) -> Rc<Promise> {
543        let name = LocalName::from(name);
544
545        // Step 1
546        if !is_valid_custom_element_name(&name) {
547            let promise = Promise::new_in_realm(realm);
548            let error = DOMException::new(
549                self.window.as_global_scope(),
550                DOMErrorName::SyntaxError,
551                CanGc::from_cx(realm),
552            );
553            promise.reject_native_with_cx(realm, &error);
554            return promise;
555        }
556
557        // Step 2
558        if let Some(definition) = self.definitions.borrow().get(&LocalName::from(&*name)) {
559            rooted!(&in(*realm) let mut constructor = UndefinedValue());
560            definition.constructor.safe_to_jsval(
561                realm.into(),
562                constructor.handle_mut(),
563                CanGc::from_cx(realm),
564            );
565            let promise = Promise::new_in_realm(realm);
566            promise.resolve_native_with_cx(realm, &constructor.get());
567            return promise;
568        }
569
570        // Steps 3, 4, 5, 6
571        let existing_promise = self.when_defined.borrow().get(&name).cloned();
572        existing_promise.unwrap_or_else(|| {
573            let promise = Promise::new_in_realm(realm);
574            self.when_defined.borrow_mut().insert(name, promise.clone());
575            promise
576        })
577    }
578
579    /// <https://html.spec.whatwg.org/multipage/#dom-customelementregistry-upgrade>
580    fn Upgrade(&self, node: &Node) {
581        // Spec says to make a list first and then iterate the list, but
582        // try-to-upgrade only queues upgrade reactions and doesn't itself
583        // modify the tree, so that's not an observable distinction.
584        node.traverse_preorder(ShadowIncluding::Yes).for_each(|n| {
585            if let Some(element) = n.downcast::<Element>() {
586                try_upgrade_element(element);
587            }
588        });
589    }
590}
591
592#[derive(Clone, JSTraceable, MallocSizeOf)]
593pub(crate) struct LifecycleCallbacks {
594    #[conditional_malloc_size_of]
595    connected_callback: Option<Rc<Function>>,
596
597    #[conditional_malloc_size_of]
598    connected_move_callback: Option<Rc<Function>>,
599
600    #[conditional_malloc_size_of]
601    disconnected_callback: Option<Rc<Function>>,
602
603    #[conditional_malloc_size_of]
604    adopted_callback: Option<Rc<Function>>,
605
606    #[conditional_malloc_size_of]
607    attribute_changed_callback: Option<Rc<Function>>,
608
609    #[conditional_malloc_size_of]
610    form_associated_callback: Option<Rc<Function>>,
611
612    #[conditional_malloc_size_of]
613    form_reset_callback: Option<Rc<Function>>,
614
615    #[conditional_malloc_size_of]
616    form_disabled_callback: Option<Rc<Function>>,
617
618    #[conditional_malloc_size_of]
619    form_state_restore_callback: Option<Rc<Function>>,
620}
621
622#[derive(Clone, JSTraceable, MallocSizeOf)]
623pub(crate) enum ConstructionStackEntry {
624    Element(DomRoot<Element>),
625    AlreadyConstructedMarker,
626}
627
628/// <https://html.spec.whatwg.org/multipage/#custom-element-definition>
629#[derive(Clone, JSTraceable, MallocSizeOf)]
630pub(crate) struct CustomElementDefinition {
631    /// <https://html.spec.whatwg.org/multipage/#concept-custom-element-definition-name>
632    #[no_trace]
633    pub(crate) name: LocalName,
634
635    /// <https://html.spec.whatwg.org/multipage/#concept-custom-element-definition-local-name>
636    #[no_trace]
637    pub(crate) local_name: LocalName,
638
639    /// <https://html.spec.whatwg.org/multipage/#concept-custom-element-definition-constructor>
640    #[conditional_malloc_size_of]
641    pub(crate) constructor: Rc<CustomElementConstructor>,
642
643    /// <https://html.spec.whatwg.org/multipage/#concept-custom-element-definition-observed-attributes>
644    pub(crate) observed_attributes: Vec<DOMString>,
645
646    /// <https://html.spec.whatwg.org/multipage/#concept-custom-element-definition-lifecycle-callbacks>
647    pub(crate) callbacks: LifecycleCallbacks,
648
649    /// <https://html.spec.whatwg.org/multipage/#concept-custom-element-definition-construction-stack>
650    pub(crate) construction_stack: DomRefCell<Vec<ConstructionStackEntry>>,
651
652    /// <https://html.spec.whatwg.org/multipage/#concept-custom-element-definition-form-associated>
653    pub(crate) form_associated: bool,
654
655    /// <https://html.spec.whatwg.org/multipage/#concept-custom-element-definition-disable-internals>
656    pub(crate) disable_internals: bool,
657
658    /// <https://html.spec.whatwg.org/multipage/#concept-custom-element-definition-disable-shadow>
659    pub(crate) disable_shadow: bool,
660}
661
662impl CustomElementDefinition {
663    #[expect(clippy::too_many_arguments)]
664    fn new(
665        name: LocalName,
666        local_name: LocalName,
667        constructor: Rc<CustomElementConstructor>,
668        observed_attributes: Vec<DOMString>,
669        callbacks: LifecycleCallbacks,
670        form_associated: bool,
671        disable_internals: bool,
672        disable_shadow: bool,
673    ) -> CustomElementDefinition {
674        CustomElementDefinition {
675            name,
676            local_name,
677            constructor,
678            observed_attributes,
679            callbacks,
680            construction_stack: Default::default(),
681            form_associated,
682            disable_internals,
683            disable_shadow,
684        }
685    }
686
687    /// <https://html.spec.whatwg.org/multipage/#autonomous-custom-element>
688    pub(crate) fn is_autonomous(&self) -> bool {
689        self.name == self.local_name
690    }
691
692    /// <https://dom.spec.whatwg.org/#concept-create-element> Step 5.1
693    #[expect(unsafe_code)]
694    pub(crate) fn create_element(
695        &self,
696        cx: &mut JSContext,
697        document: &Document,
698        prefix: Option<Prefix>,
699        registry: Option<DomRoot<CustomElementRegistry>>,
700    ) -> Fallible<DomRoot<Element>> {
701        let window = document.window();
702
703        // Step 5.1.1. Let C be definition’s constructor.
704        rooted!(&in(cx) let constructor = ObjectValue(self.constructor.callback()));
705        rooted!(&in(cx) let mut element = ptr::null_mut::<JSObject>());
706        {
707            // Go into the constructor's realm
708            let mut realm = AutoRealm::new(cx, NonNull::new(self.constructor.callback()).unwrap());
709            let cx = &mut realm;
710
711            // Step 5.3.1. Set result to the result of constructing C, with no arguments.
712            // https://webidl.spec.whatwg.org/#construct-a-callback-function
713            run_a_script::<DomTypeHolder, _>(window.upcast(), || {
714                run_a_callback::<DomTypeHolder, _>(window.upcast(), || {
715                    let args = HandleValueArray::empty();
716                    if unsafe { !Construct1(cx, constructor.handle(), &args, element.handle_mut()) }
717                    {
718                        Err(Error::JSFailed)
719                    } else {
720                        Ok(())
721                    }
722                })
723            })?;
724        }
725
726        rooted!(&in(cx) let element_val = ObjectValue(element.get()));
727        let element: DomRoot<Element> =
728            match FromJSValConvertible::safe_from_jsval(cx, element_val.handle(), ()) {
729                Ok(ConversionResult::Success(element)) => element,
730                Ok(ConversionResult::Failure(..)) => {
731                    return Err(Error::Type(
732                        c"Constructor did not return a DOM node".to_owned(),
733                    ));
734                },
735                _ => return Err(Error::JSFailed),
736            };
737
738        // Step 5.1.3.2 Assert: result’s custom element state and custom element definition are initialized.
739        // Step 5.1.3.3 Assert: result’s namespace is the HTML namespace.
740        // Note: IDL enforces that result is an HTMLElement object, which all use the HTML namespace.
741        // Note: the custom element definition is initialized by the caller if
742        // this method returns a success value.
743        assert!(element.is::<HTMLElement>());
744
745        // Step 5.1.3.4. If result’s attribute list is not empty, then throw a "NotSupportedError" DOMException.
746        // Step 5.1.3.5. If result has children, then throw a "NotSupportedError" DOMException.
747        // Step 5.1.3.6. If result’s parent is not null, then throw a "NotSupportedError" DOMException.
748        // Step 5.1.3.7. If result’s node document is not document, then throw a "NotSupportedError" DOMException.
749        // Step 5.1.3.8. If result’s local name is not equal to localName then throw a "NotSupportedError" DOMException.
750        if element.HasAttributes() ||
751            element.upcast::<Node>().children_count() > 0 ||
752            element.upcast::<Node>().has_parent() ||
753            &*element.upcast::<Node>().owner_doc() != document ||
754            *element.namespace() != ns!(html) ||
755            *element.local_name() != self.local_name
756        {
757            return Err(Error::NotSupported(None));
758        }
759
760        // Step 5.1.3.9. Set result’s namespace prefix to prefix.
761        element.set_prefix(prefix);
762
763        // Step 5.1.3.10. Set result’s is value to null.
764        // Element's `is` is None by default
765
766        // Step 5.1.3.11. Set result’s custom element registry to registry.
767        element.set_custom_element_registry(registry);
768
769        Ok(element)
770    }
771
772    pub(crate) fn has_attribute_changed_callback(&self) -> bool {
773        self.callbacks.attribute_changed_callback.is_some()
774    }
775}
776
777/// <https://html.spec.whatwg.org/multipage/#concept-upgrade-an-element>
778pub(crate) fn upgrade_element(
779    cx: &mut JSContext,
780    definition: Rc<CustomElementDefinition>,
781    element: &Element,
782) {
783    // Step 1. If element's custom element state is not "undefined" or "uncustomized", then return.
784    let state = element.get_custom_element_state();
785    if state != CustomElementState::Undefined && state != CustomElementState::Uncustomized {
786        return;
787    }
788
789    // Step 2. Set element's custom element definition to definition.
790    element.set_custom_element_definition(Rc::clone(&definition));
791
792    // Step 3. Set element's custom element state to "failed".
793    element.set_custom_element_state(CustomElementState::Failed);
794
795    // Step 4. For each attribute in element's attribute list, in order, enqueue a custom element callback reaction
796    // with element, callback name "attributeChangedCallback", and « attribute's local name, null, attribute's value,
797    // attribute's namespace ».
798    let custom_element_reaction_stack = ScriptThread::custom_element_reaction_stack();
799    for attr in element.attrs().borrow().iter() {
800        let local_name = attr.local_name().clone();
801        let namespace = attr.namespace().clone();
802        custom_element_reaction_stack.enqueue_callback_reaction(
803            element,
804            CallbackReaction::AttributeChanged(local_name, None, Some(&*attr.value()), namespace),
805            Some(definition.clone()),
806        );
807    }
808
809    // Step 5. If element is connected, then enqueue a custom element callback reaction with element,
810    // callback name "connectedCallback", and « ».
811    if element.is_connected() {
812        ScriptThread::enqueue_callback_reaction(
813            element,
814            CallbackReaction::Connected,
815            Some(definition.clone()),
816        );
817    }
818
819    // Step 6. Add element to the end of definition's construction stack.
820    definition
821        .construction_stack
822        .borrow_mut()
823        .push(ConstructionStackEntry::Element(DomRoot::from_ref(element)));
824
825    // Steps 7-8, successful case
826    let result = run_upgrade_constructor(cx, &definition, element);
827
828    // "regardless of whether the above steps threw an exception" step
829    definition.construction_stack.borrow_mut().pop();
830
831    // Step 8 exception handling
832    if let Err(error) = result {
833        // Step 8.exception.1
834        element.clear_custom_element_definition();
835
836        // Step 8.exception.2
837        element.clear_reaction_queue();
838
839        // Step 8.exception.3
840        let global = GlobalScope::current().expect("No current global");
841
842        let mut realm = enter_auto_realm(cx, &*global);
843        let cx = &mut realm.current_realm();
844
845        throw_dom_exception(cx.into(), &global, error, CanGc::from_cx(cx));
846        report_pending_exception(cx);
847
848        return;
849    }
850
851    // Step 9: handle with form-associated custom element
852    if let Some(html_element) = element.downcast::<HTMLElement>() &&
853        html_element.is_form_associated_custom_element()
854    {
855        // We know this element is is form-associated, so we can use the implementation of
856        // `FormControl` for HTMLElement, which makes that assumption.
857        // Step 9.1: Reset the form owner of element
858        html_element.reset_form_owner(cx);
859        if let Some(form) = html_element.form_owner() {
860            // Even though the tree hasn't structurally mutated,
861            // HTMLCollections need to be invalidated.
862            form.upcast::<Node>().rev_version();
863            // The spec tells us specifically to enqueue a formAssociated reaction
864            // here, but it also says to do that for resetting form owner in general,
865            // and we don't need two reactions.
866        }
867
868        // Either enabled_state or disabled_state needs to be set,
869        // and the possibility of a disabled fieldset ancestor needs
870        // to be accounted for. (In the spec, being disabled is
871        // a fact that's true or false about a node at a given time,
872        // not a flag that belongs to the node and is updated,
873        // so it doesn't describe this check as an action.)
874        element.check_disabled_attribute();
875        element.check_ancestors_disabled_state_for_form_control();
876        element.update_read_write_state_from_readonly_attribute();
877
878        // Step 9.2: If element is disabled, then enqueue a custom element callback reaction
879        // with element.
880        if element.disabled_state() {
881            ScriptThread::enqueue_callback_reaction(
882                element,
883                CallbackReaction::FormDisabled(true),
884                Some(definition),
885            )
886        }
887    }
888
889    // Step 10
890    element.set_custom_element_state(CustomElementState::Custom);
891}
892
893/// <https://html.spec.whatwg.org/multipage/#concept-upgrade-an-element>
894/// Steps 9.1-9.4
895#[expect(unsafe_code)]
896fn run_upgrade_constructor(
897    cx: &mut JSContext,
898    definition: &CustomElementDefinition,
899    element: &Element,
900) -> ErrorResult {
901    let constructor = &definition.constructor;
902    let window = element.owner_window();
903    rooted!(&in(cx) let constructor_val = ObjectValue(constructor.callback()));
904    rooted!(&in(cx) let mut element_val = UndefinedValue());
905    element.safe_to_jsval(cx.into(), element_val.handle_mut(), CanGc::from_cx(cx));
906    rooted!(&in(cx) let mut construct_result = ptr::null_mut::<JSObject>());
907    {
908        // Step 9.1. If definition's disable shadow is true and element's shadow root is non-null,
909        // then throw a "NotSupportedError" DOMException.
910        if definition.disable_shadow && element.is_shadow_host() {
911            return Err(Error::NotSupported(None));
912        }
913
914        // Go into the constructor's realm
915        let mut realm = AutoRealm::new(cx, NonNull::new(constructor.callback()).unwrap());
916        let cx = &mut *realm;
917
918        let args = HandleValueArray::empty();
919        // Step 8.2. Set element's custom element state to "precustomized".
920        element.set_custom_element_state(CustomElementState::Precustomized);
921
922        // Step 9.3. Let constructResult be the result of constructing C, with no arguments.
923        // https://webidl.spec.whatwg.org/#construct-a-callback-function
924        run_a_script::<DomTypeHolder, _>(window.upcast(), || {
925            run_a_callback::<DomTypeHolder, _>(window.upcast(), || {
926                if unsafe {
927                    !Construct1(
928                        cx,
929                        constructor_val.handle(),
930                        &args,
931                        construct_result.handle_mut(),
932                    )
933                } {
934                    Err(Error::JSFailed)
935                } else {
936                    Ok(())
937                }
938            })
939        })?;
940
941        let mut same = false;
942        rooted!(&in(cx) let construct_result_val = ObjectValue(construct_result.get()));
943
944        // Step 9.4. If SameValue(constructResult, element) is false, then throw a TypeError.
945        if unsafe {
946            !SameValue(
947                cx,
948                construct_result_val.handle(),
949                element_val.handle(),
950                &mut same,
951            )
952        } {
953            return Err(Error::JSFailed);
954        }
955        if !same {
956            return Err(Error::Type(
957                c"Returned element is not SameValue as the upgraded element".to_owned(),
958            ));
959        }
960    }
961    Ok(())
962}
963
964/// <https://html.spec.whatwg.org/multipage/#concept-try-upgrade>
965pub(crate) fn try_upgrade_element(element: &Element) {
966    // Step 1. Let definition be the result of looking up a custom element definition given element's node document,
967    // element's namespace, element's local name, and element's is value.
968    let document = element.owner_document();
969    let namespace = element.namespace();
970    let local_name = element.local_name();
971    let is = element.get_is();
972    if let Some(definition) =
973        document.lookup_custom_element_definition(namespace, local_name, is.as_ref())
974    {
975        // Step 2. If definition is not null, then enqueue a custom element upgrade reaction given
976        // element and definition.
977        ScriptThread::enqueue_upgrade_reaction(element, definition);
978    }
979}
980
981#[derive(JSTraceable, MallocSizeOf)]
982#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
983pub(crate) enum CustomElementReaction {
984    Upgrade(#[conditional_malloc_size_of] Rc<CustomElementDefinition>),
985    Callback(
986        #[conditional_malloc_size_of] Rc<Function>,
987        #[ignore_malloc_size_of = "mozjs"] Box<[Heap<JSVal>]>,
988    ),
989}
990
991impl CustomElementReaction {
992    /// <https://html.spec.whatwg.org/multipage/#invoke-custom-element-reactions>
993    pub(crate) fn invoke(&self, cx: &mut JSContext, element: &Element) {
994        // Step 2.1
995        match *self {
996            CustomElementReaction::Upgrade(ref definition) => {
997                upgrade_element(cx, definition.clone(), element)
998            },
999            CustomElementReaction::Callback(ref callback, ref arguments) => {
1000                // We're rooted, so it's safe to hand out a handle to objects in Heap
1001                let arguments = arguments.iter().map(|arg| arg.as_handle_value()).collect();
1002                rooted!(&in(cx) let mut value: JSVal);
1003                let _ = callback.Call_(
1004                    cx,
1005                    element,
1006                    arguments,
1007                    value.handle_mut(),
1008                    ExceptionHandling::Report,
1009                );
1010            },
1011        }
1012    }
1013}
1014
1015pub(crate) enum CallbackReaction<'a> {
1016    Connected,
1017    Disconnected,
1018    Adopted(DomRoot<Document>, DomRoot<Document>),
1019    AttributeChanged(
1020        LocalName,
1021        Option<&'a AttrValue>,
1022        Option<&'a AttrValue>,
1023        Namespace,
1024    ),
1025    FormAssociated(Option<DomRoot<HTMLFormElement>>),
1026    FormDisabled(bool),
1027    FormReset,
1028    ConnectedMove,
1029}
1030
1031/// <https://html.spec.whatwg.org/multipage/#processing-the-backup-element-queue>
1032#[derive(Clone, Copy, Eq, JSTraceable, MallocSizeOf, PartialEq)]
1033enum BackupElementQueueFlag {
1034    Processing,
1035    NotProcessing,
1036}
1037
1038/// <https://html.spec.whatwg.org/multipage/#custom-element-reactions-stack>
1039/// # Safety
1040/// This can be shared inside an Rc because one of those Rc copies lives
1041/// inside ScriptThread, so the GC can always reach this structure.
1042#[derive(JSTraceable, MallocSizeOf)]
1043#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
1044#[cfg_attr(crown, crown::unrooted_must_root_lint::allow_unrooted_in_rc)]
1045pub(crate) struct CustomElementReactionStack {
1046    stack: DomRefCell<Vec<ElementQueue>>,
1047    backup_queue: ElementQueue,
1048    processing_backup_element_queue: Cell<BackupElementQueueFlag>,
1049}
1050
1051impl CustomElementReactionStack {
1052    pub(crate) fn new() -> CustomElementReactionStack {
1053        CustomElementReactionStack {
1054            stack: DomRefCell::new(Vec::new()),
1055            backup_queue: ElementQueue::new(),
1056            processing_backup_element_queue: Cell::new(BackupElementQueueFlag::NotProcessing),
1057        }
1058    }
1059
1060    pub(crate) fn push_new_element_queue(&self) {
1061        self.stack.borrow_mut().push(ElementQueue::new());
1062    }
1063
1064    pub(crate) fn pop_current_element_queue(&self, cx: &mut JSContext) {
1065        rooted_vec!(let mut stack);
1066        mem::swap(&mut *stack, &mut *self.stack.borrow_mut());
1067
1068        if let Some(current_queue) = stack.last() {
1069            current_queue.invoke_reactions(cx);
1070        }
1071        stack.pop();
1072
1073        mem::swap(&mut *self.stack.borrow_mut(), &mut *stack);
1074        self.stack.borrow_mut().append(&mut *stack);
1075    }
1076
1077    /// <https://html.spec.whatwg.org/multipage/#enqueue-an-element-on-the-appropriate-element-queue>
1078    /// Step 4
1079    pub(crate) fn invoke_backup_element_queue(&self, cx: &mut JSContext) {
1080        // Step 4.1
1081        self.backup_queue.invoke_reactions(cx);
1082
1083        // Step 4.2
1084        self.processing_backup_element_queue
1085            .set(BackupElementQueueFlag::NotProcessing);
1086    }
1087
1088    /// <https://html.spec.whatwg.org/multipage/#enqueue-an-element-on-the-appropriate-element-queue>
1089    pub(crate) fn enqueue_element(&self, element: &Element) {
1090        if let Some(current_queue) = self.stack.borrow().last() {
1091            // Step 2
1092            current_queue.append_element(element);
1093        } else {
1094            // Step 1.1
1095            self.backup_queue.append_element(element);
1096
1097            // Step 1.2
1098            if self.processing_backup_element_queue.get() == BackupElementQueueFlag::Processing {
1099                return;
1100            }
1101
1102            // Step 1.3
1103            self.processing_backup_element_queue
1104                .set(BackupElementQueueFlag::Processing);
1105
1106            // Step 4
1107            ScriptThread::enqueue_microtask(Microtask::CustomElementReaction);
1108        }
1109    }
1110
1111    /// <https://html.spec.whatwg.org/multipage/#enqueue-a-custom-element-callback-reaction>
1112    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
1113    pub(crate) fn enqueue_callback_reaction(
1114        &self,
1115        element: &Element,
1116        reaction: CallbackReaction,
1117        definition: Option<Rc<CustomElementDefinition>>,
1118    ) {
1119        // Step 1. Let definition be element's custom element definition.
1120        let definition = match definition.or_else(|| element.get_custom_element_definition()) {
1121            Some(definition) => definition,
1122            None => return,
1123        };
1124
1125        // Step 2. Let callback be the value of the entry in definition's lifecycle callbacks with
1126        // key callbackName.
1127        let (callback, args) = match reaction {
1128            CallbackReaction::Connected => {
1129                (definition.callbacks.connected_callback.clone(), Vec::new())
1130            },
1131            CallbackReaction::Disconnected => (
1132                definition.callbacks.disconnected_callback.clone(),
1133                Vec::new(),
1134            ),
1135            CallbackReaction::Adopted(ref old_doc, ref new_doc) => {
1136                let args = vec![Heap::default(), Heap::default()];
1137                args[0].set(ObjectValue(old_doc.reflector().get_jsobject().get()));
1138                args[1].set(ObjectValue(new_doc.reflector().get_jsobject().get()));
1139                (definition.callbacks.adopted_callback.clone(), args)
1140            },
1141            CallbackReaction::AttributeChanged(local_name, old_val, val, namespace) => {
1142                // Step 5.
1143                if !definition
1144                    .observed_attributes
1145                    .iter()
1146                    .any(|attr| *attr == *local_name)
1147                {
1148                    return;
1149                }
1150
1151                let cx = GlobalScope::get_cx();
1152                // We might be here during HTML parsing, rather than
1153                // during Javscript execution, and so we typically aren't
1154                // already in a realm here.
1155                let _ac = JSAutoRealm::new(*cx, element.global().reflector().get_jsobject().get());
1156
1157                let local_name = DOMString::from(&*local_name);
1158                rooted!(in(*cx) let mut name_value = UndefinedValue());
1159                local_name.safe_to_jsval(cx, name_value.handle_mut(), CanGc::deprecated_note());
1160
1161                rooted!(in(*cx) let mut old_value = NullValue());
1162                if let Some(old_val) = old_val {
1163                    old_val.safe_to_jsval(cx, old_value.handle_mut(), CanGc::deprecated_note());
1164                }
1165
1166                rooted!(in(*cx) let mut value = NullValue());
1167                if let Some(val) = val {
1168                    val.safe_to_jsval(cx, value.handle_mut(), CanGc::deprecated_note());
1169                }
1170
1171                rooted!(in(*cx) let mut namespace_value = NullValue());
1172                if namespace != ns!() {
1173                    let namespace = DOMString::from(&*namespace);
1174                    namespace.safe_to_jsval(
1175                        cx,
1176                        namespace_value.handle_mut(),
1177                        CanGc::deprecated_note(),
1178                    );
1179                }
1180
1181                let args = vec![
1182                    Heap::default(),
1183                    Heap::default(),
1184                    Heap::default(),
1185                    Heap::default(),
1186                ];
1187                args[0].set(name_value.get());
1188                args[1].set(old_value.get());
1189                args[2].set(value.get());
1190                args[3].set(namespace_value.get());
1191
1192                (
1193                    definition.callbacks.attribute_changed_callback.clone(),
1194                    args,
1195                )
1196            },
1197            CallbackReaction::FormAssociated(form) => {
1198                let args = vec![Heap::default()];
1199                if let Some(form) = form {
1200                    args[0].set(ObjectValue(form.reflector().get_jsobject().get()));
1201                } else {
1202                    args[0].set(NullValue());
1203                }
1204                (definition.callbacks.form_associated_callback.clone(), args)
1205            },
1206            CallbackReaction::FormDisabled(disabled) => {
1207                let cx = GlobalScope::get_cx();
1208                rooted!(in(*cx) let mut disabled_value = BooleanValue(disabled));
1209                let args = vec![Heap::default()];
1210                args[0].set(disabled_value.get());
1211                (definition.callbacks.form_disabled_callback.clone(), args)
1212            },
1213            CallbackReaction::FormReset => {
1214                (definition.callbacks.form_reset_callback.clone(), Vec::new())
1215            },
1216            CallbackReaction::ConnectedMove => {
1217                let callback = definition.callbacks.connected_move_callback.clone();
1218                // Step 3. If callbackName is "connectedMoveCallback" and callback is null:
1219                if callback.is_none() {
1220                    // Step 3.1. Let disconnectedCallback be the value of the entry in
1221                    // definition's lifecycle callbacks with key "disconnectedCallback".
1222                    let disconnected_callback = definition.callbacks.disconnected_callback.clone();
1223
1224                    // Step 3.2. Let connectedCallback be the value of the entry in
1225                    // definition's lifecycle callbacks with key "connectedCallback".
1226                    let connected_callback = definition.callbacks.connected_callback.clone();
1227
1228                    // Step 3.3. If connectedCallback and disconnectedCallback are null,
1229                    // then return.
1230                    if disconnected_callback.is_none() && connected_callback.is_none() {
1231                        return;
1232                    }
1233
1234                    // Step 3.4. Set callback to the following steps:
1235                    // Step 3.4.1. If disconnectedCallback is not null, then call
1236                    // disconnectedCallback with no arguments.
1237                    if let Some(disconnected_callback) = disconnected_callback {
1238                        element.push_callback_reaction(disconnected_callback, Box::new([]));
1239                    }
1240                    // Step 3.4.2. If connectedCallback is not null, then call
1241                    // connectedCallback with no arguments.
1242                    if let Some(connected_callback) = connected_callback {
1243                        element.push_callback_reaction(connected_callback, Box::new([]));
1244                    }
1245
1246                    self.enqueue_element(element);
1247                    return;
1248                }
1249
1250                (callback, Vec::new())
1251            },
1252        };
1253
1254        // Step 4. If callback is null, then return.
1255        let callback = match callback {
1256            Some(callback) => callback,
1257            None => return,
1258        };
1259
1260        // Step 6. Add a new callback reaction to element's custom element reaction queue, with
1261        // callback function callback and arguments args.
1262        element.push_callback_reaction(callback, args.into_boxed_slice());
1263
1264        // Step 7. Enqueue an element on the appropriate element queue given element.
1265        self.enqueue_element(element);
1266    }
1267
1268    /// <https://html.spec.whatwg.org/multipage/#enqueue-a-custom-element-upgrade-reaction>
1269    pub(crate) fn enqueue_upgrade_reaction(
1270        &self,
1271        element: &Element,
1272        definition: Rc<CustomElementDefinition>,
1273    ) {
1274        // Step 1. Add a new upgrade reaction to element's custom element reaction queue,
1275        // with custom element definition definition.
1276        element.push_upgrade_reaction(definition);
1277
1278        // Step 2. Enqueue an element on the appropriate element queue given element.
1279        self.enqueue_element(element);
1280    }
1281}
1282
1283/// <https://html.spec.whatwg.org/multipage/#element-queue>
1284#[derive(JSTraceable, MallocSizeOf)]
1285#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
1286struct ElementQueue {
1287    queue: DomRefCell<VecDeque<Dom<Element>>>,
1288}
1289
1290impl ElementQueue {
1291    fn new() -> ElementQueue {
1292        ElementQueue {
1293            queue: Default::default(),
1294        }
1295    }
1296
1297    /// <https://html.spec.whatwg.org/multipage/#invoke-custom-element-reactions>
1298    fn invoke_reactions(&self, cx: &mut JSContext) {
1299        // Steps 1-2
1300        while let Some(element) = self.next_element() {
1301            element.invoke_reactions(cx)
1302        }
1303        self.queue.borrow_mut().clear();
1304    }
1305
1306    fn next_element(&self) -> Option<DomRoot<Element>> {
1307        self.queue
1308            .borrow_mut()
1309            .pop_front()
1310            .as_deref()
1311            .map(DomRoot::from_ref)
1312    }
1313
1314    fn append_element(&self, element: &Element) {
1315        self.queue.borrow_mut().push_back(Dom::from_ref(element));
1316    }
1317}
1318
1319/// <https://html.spec.whatwg.org/multipage/#valid-custom-element-name>
1320pub(crate) fn is_valid_custom_element_name(name: &str) -> bool {
1321    // Custom elment names must match:
1322    // PotentialCustomElementName ::= [a-z] (PCENChar)* '-' (PCENChar)*
1323    let mut chars = name.chars();
1324    if !chars.next().is_some_and(|c| c.is_ascii_lowercase()) {
1325        return false;
1326    }
1327
1328    let mut has_dash = false;
1329
1330    for c in chars {
1331        if c == '-' {
1332            has_dash = true;
1333            continue;
1334        }
1335
1336        if !is_potential_custom_element_char(c) {
1337            return false;
1338        }
1339    }
1340
1341    if !has_dash {
1342        return false;
1343    }
1344
1345    if name == "annotation-xml" ||
1346        name == "color-profile" ||
1347        name == "font-face" ||
1348        name == "font-face-src" ||
1349        name == "font-face-uri" ||
1350        name == "font-face-format" ||
1351        name == "font-face-name" ||
1352        name == "missing-glyph"
1353    {
1354        return false;
1355    }
1356
1357    true
1358}
1359
1360/// Check if this character is a PCENChar
1361/// <https://html.spec.whatwg.org/multipage/#prod-pcenchar>
1362fn is_potential_custom_element_char(c: char) -> bool {
1363    c == '-' ||
1364        c == '.' ||
1365        c == '_' ||
1366        c == '\u{B7}' ||
1367        c.is_ascii_digit() ||
1368        c.is_ascii_lowercase() ||
1369        ('\u{C0}'..='\u{D6}').contains(&c) ||
1370        ('\u{D8}'..='\u{F6}').contains(&c) ||
1371        ('\u{F8}'..='\u{37D}').contains(&c) ||
1372        ('\u{37F}'..='\u{1FFF}').contains(&c) ||
1373        ('\u{200C}'..='\u{200D}').contains(&c) ||
1374        ('\u{203F}'..='\u{2040}').contains(&c) ||
1375        ('\u{2070}'..='\u{218F}').contains(&c) ||
1376        ('\u{2C00}'..='\u{2FEF}').contains(&c) ||
1377        ('\u{3001}'..='\u{D7FF}').contains(&c) ||
1378        ('\u{F900}'..='\u{FDCF}').contains(&c) ||
1379        ('\u{FDF0}'..='\u{FFFD}').contains(&c) ||
1380        ('\u{10000}'..='\u{EFFFF}').contains(&c)
1381}
1382
1383fn is_extendable_element_interface(element: &str) -> bool {
1384    element == "a" ||
1385        element == "abbr" ||
1386        element == "acronym" ||
1387        element == "address" ||
1388        element == "area" ||
1389        element == "article" ||
1390        element == "aside" ||
1391        element == "audio" ||
1392        element == "b" ||
1393        element == "base" ||
1394        element == "bdi" ||
1395        element == "bdo" ||
1396        element == "big" ||
1397        element == "blockquote" ||
1398        element == "body" ||
1399        element == "br" ||
1400        element == "button" ||
1401        element == "canvas" ||
1402        element == "caption" ||
1403        element == "center" ||
1404        element == "cite" ||
1405        element == "code" ||
1406        element == "col" ||
1407        element == "colgroup" ||
1408        element == "data" ||
1409        element == "datalist" ||
1410        element == "dd" ||
1411        element == "del" ||
1412        element == "details" ||
1413        element == "dfn" ||
1414        element == "dialog" ||
1415        element == "dir" ||
1416        element == "div" ||
1417        element == "dl" ||
1418        element == "dt" ||
1419        element == "em" ||
1420        element == "embed" ||
1421        element == "fieldset" ||
1422        element == "figcaption" ||
1423        element == "figure" ||
1424        element == "font" ||
1425        element == "footer" ||
1426        element == "form" ||
1427        element == "frame" ||
1428        element == "frameset" ||
1429        element == "h1" ||
1430        element == "h2" ||
1431        element == "h3" ||
1432        element == "h4" ||
1433        element == "h5" ||
1434        element == "h6" ||
1435        element == "head" ||
1436        element == "header" ||
1437        element == "hgroup" ||
1438        element == "hr" ||
1439        element == "html" ||
1440        element == "i" ||
1441        element == "iframe" ||
1442        element == "img" ||
1443        element == "input" ||
1444        element == "ins" ||
1445        element == "kbd" ||
1446        element == "label" ||
1447        element == "legend" ||
1448        element == "li" ||
1449        element == "link" ||
1450        element == "listing" ||
1451        element == "main" ||
1452        element == "map" ||
1453        element == "mark" ||
1454        element == "marquee" ||
1455        element == "menu" ||
1456        element == "meta" ||
1457        element == "meter" ||
1458        element == "nav" ||
1459        element == "nobr" ||
1460        element == "noframes" ||
1461        element == "noscript" ||
1462        element == "object" ||
1463        element == "ol" ||
1464        element == "optgroup" ||
1465        element == "option" ||
1466        element == "output" ||
1467        element == "p" ||
1468        element == "param" ||
1469        element == "picture" ||
1470        element == "plaintext" ||
1471        element == "pre" ||
1472        element == "progress" ||
1473        element == "q" ||
1474        element == "rp" ||
1475        element == "rt" ||
1476        element == "ruby" ||
1477        element == "s" ||
1478        element == "samp" ||
1479        element == "script" ||
1480        element == "section" ||
1481        element == "select" ||
1482        element == "slot" ||
1483        element == "small" ||
1484        element == "source" ||
1485        element == "span" ||
1486        element == "strike" ||
1487        element == "strong" ||
1488        element == "style" ||
1489        element == "sub" ||
1490        element == "summary" ||
1491        element == "sup" ||
1492        element == "table" ||
1493        element == "tbody" ||
1494        element == "td" ||
1495        element == "template" ||
1496        element == "textarea" ||
1497        element == "tfoot" ||
1498        element == "th" ||
1499        element == "thead" ||
1500        element == "time" ||
1501        element == "title" ||
1502        element == "tr" ||
1503        element == "tt" ||
1504        element == "track" ||
1505        element == "u" ||
1506        element == "ul" ||
1507        element == "var" ||
1508        element == "video" ||
1509        element == "wbr" ||
1510        element == "xmp"
1511}