servo: Merge #17112 - Implement custom element registry (from cbrewster:custom_element_registry); r=jdm
authorConnor Brewster <connor.brewster@eagles.oc.edu>
Mon, 05 Jun 2017 08:38:03 -0700
changeset 410489 a7ea563ab337e8f8b889df2a135007c896fea022
parent 410488 f9eddc0d3345f353f4155521ae1234b2df6aa6e8
child 410490 2f1c4c2910386dee53b0bfb632b958a93d1c4d6f
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdm
bugs17112, 16753
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
servo: Merge #17112 - Implement custom element registry (from cbrewster:custom_element_registry); r=jdm <!-- Please describe your changes on the following line: --> Implements https://html.spec.whatwg.org/multipage/#customelementregistry --- <!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `__` with appropriate data: --> - [X] `./mach build -d` does not report any errors - [X] `./mach test-tidy` does not report any errors - [X] These changes fix #16753 (github issue number if applicable). <!-- Either: --> - [X] There are tests for these changes OR - [ ] These changes do not require tests because _____ <!-- Also, please make sure that "Allow edits from maintainers" checkbox is checked, so that we can help you if you get stuck somewhere along the way.--> <!-- Pull requests that do not address these steps are welcome, but they will require additional verification as part of the review process. --> Source-Repo: https://github.com/servo/servo Source-Revision: b584944f1731c71920abf9a03e1f3cd1e790f7c4
servo/components/script/dom/customelementregistry.rs
servo/components/script/dom/mod.rs
servo/components/script/dom/webidls/CustomElementRegistry.webidl
servo/components/script/dom/webidls/Window.webidl
servo/components/script/dom/window.rs
servo/resources/prefs.json
new file mode 100644
--- /dev/null
+++ b/servo/components/script/dom/customelementregistry.rs
@@ -0,0 +1,432 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use dom::bindings::callback::CallbackContainer;
+use dom::bindings::cell::DOMRefCell;
+use dom::bindings::codegen::Bindings::CustomElementRegistryBinding;
+use dom::bindings::codegen::Bindings::CustomElementRegistryBinding::CustomElementRegistryMethods;
+use dom::bindings::codegen::Bindings::CustomElementRegistryBinding::ElementDefinitionOptions;
+use dom::bindings::codegen::Bindings::FunctionBinding::Function;
+use dom::bindings::error::{Error, ErrorResult};
+use dom::bindings::inheritance::Castable;
+use dom::bindings::js::{JS, Root};
+use dom::bindings::reflector::{DomObject, Reflector, reflect_dom_object};
+use dom::bindings::str::DOMString;
+use dom::domexception::{DOMErrorName, DOMException};
+use dom::globalscope::GlobalScope;
+use dom::promise::Promise;
+use dom::window::Window;
+use dom_struct::dom_struct;
+use js::conversions::ToJSValConvertible;
+use js::jsapi::{IsConstructor, HandleObject, JS_GetProperty, JSAutoCompartment, JSContext};
+use js::jsval::{JSVal, UndefinedValue};
+use std::cell::Cell;
+use std::collections::HashMap;
+use std::rc::Rc;
+
+// https://html.spec.whatwg.org/multipage/#customelementregistry
+#[dom_struct]
+pub struct CustomElementRegistry {
+    reflector_: Reflector,
+
+    window: JS<Window>,
+
+    #[ignore_heap_size_of = "Rc"]
+    when_defined: DOMRefCell<HashMap<DOMString, Rc<Promise>>>,
+
+    element_definition_is_running: Cell<bool>,
+
+    definitions: DOMRefCell<HashMap<DOMString, CustomElementDefinition>>,
+}
+
+impl CustomElementRegistry {
+    fn new_inherited(window: &Window) -> CustomElementRegistry {
+        CustomElementRegistry {
+            reflector_: Reflector::new(),
+            window: JS::from_ref(window),
+            when_defined: DOMRefCell::new(HashMap::new()),
+            element_definition_is_running: Cell::new(false),
+            definitions: DOMRefCell::new(HashMap::new()),
+        }
+    }
+
+    pub fn new(window: &Window) -> Root<CustomElementRegistry> {
+        reflect_dom_object(box CustomElementRegistry::new_inherited(window),
+                           window,
+                           CustomElementRegistryBinding::Wrap)
+    }
+
+    // Cleans up any active promises
+    // https://github.com/servo/servo/issues/15318
+    pub fn teardown(&self) {
+        self.when_defined.borrow_mut().clear()
+    }
+
+    // https://html.spec.whatwg.org/multipage/#dom-customelementregistry-define
+    // Steps 10.1, 10.2
+    #[allow(unsafe_code)]
+    fn check_prototype(&self, constructor: HandleObject) -> ErrorResult {
+        let global_scope = self.window.upcast::<GlobalScope>();
+        rooted!(in(global_scope.get_cx()) let mut prototype = UndefinedValue());
+        unsafe {
+            // Step 10.1
+            if !JS_GetProperty(global_scope.get_cx(),
+                               constructor,
+                               b"prototype\0".as_ptr() as *const _,
+                               prototype.handle_mut()) {
+                return Err(Error::JSFailed);
+            }
+
+            // Step 10.2
+            if !prototype.is_object() {
+                return Err(Error::Type("constructor.prototype is not an object".to_owned()));
+            }
+        }
+        Ok(())
+    }
+}
+
+impl CustomElementRegistryMethods for CustomElementRegistry {
+    #[allow(unsafe_code, unrooted_must_root)]
+    // https://html.spec.whatwg.org/multipage/#dom-customelementregistry-define
+    fn Define(&self, name: DOMString, constructor_: Rc<Function>, options: &ElementDefinitionOptions) -> ErrorResult {
+        let global_scope = self.window.upcast::<GlobalScope>();
+        rooted!(in(global_scope.get_cx()) let constructor = constructor_.callback());
+
+        // Step 1
+        if unsafe { !IsConstructor(constructor.get()) } {
+            return Err(Error::Type("Second argument of CustomElementRegistry.define is not a constructor".to_owned()));
+        }
+
+        // Step 2
+        if !is_valid_custom_element_name(&name) {
+            return Err(Error::Syntax)
+        }
+
+        // Step 3
+        if self.definitions.borrow().contains_key(&name) {
+            return Err(Error::NotSupported);
+        }
+
+        // Step 4
+        if self.definitions.borrow().iter().any(|(_, ref def)| def.constructor == constructor_) {
+            return Err(Error::NotSupported);
+        }
+
+        // Step 6
+        let extends = &options.extends;
+
+        // Steps 5, 7
+        let local_name = if let Some(ref extended_name) = *extends {
+            // Step 7.1
+            if is_valid_custom_element_name(extended_name) {
+                return Err(Error::NotSupported)
+            }
+
+            // Step 7.2
+            if !is_known_element_interface(extended_name) {
+                return Err(Error::NotSupported)
+            }
+
+            extended_name.clone()
+        } else {
+            // Step 7.3
+            name.clone()
+        };
+
+        // Step 8
+        if self.element_definition_is_running.get() {
+            return Err(Error::NotSupported);
+        }
+
+        // Step 9
+        self.element_definition_is_running.set(true);
+
+        // Steps 10.1 - 10.2
+        let result = {
+            let _ac = JSAutoCompartment::new(global_scope.get_cx(), constructor.get());
+            self.check_prototype(constructor.handle())
+        };
+
+        // TODO: Steps 10.3 - 10.6
+        // 10.3 - 10.4 Handle lifecycle callbacks
+        // 10.5 - 10.6 Get observed attributes from the constructor
+
+        self.element_definition_is_running.set(false);
+        result?;
+
+        // Step 11
+        let definition = CustomElementDefinition::new(name.clone(), local_name, constructor_);
+
+        // Step 12
+        self.definitions.borrow_mut().insert(name.clone(), definition);
+
+        // TODO: Step 13, 14, 15
+        // Handle custom element upgrades
+
+        // Step 16, 16.3
+        if let Some(promise) = self.when_defined.borrow_mut().remove(&name) {
+            // 16.1
+            let cx = promise.global().get_cx();
+            // 16.2
+            promise.resolve_native(cx, &UndefinedValue());
+        }
+        Ok(())
+    }
+
+    // https://html.spec.whatwg.org/multipage/#dom-customelementregistry-get
+    #[allow(unsafe_code)]
+    unsafe fn Get(&self, cx: *mut JSContext, name: DOMString) -> JSVal {
+        match self.definitions.borrow().get(&name) {
+            Some(definition) => {
+                rooted!(in(cx) let mut constructor = UndefinedValue());
+                definition.constructor.to_jsval(cx, constructor.handle_mut());
+                constructor.get()
+            },
+            None => UndefinedValue(),
+        }
+    }
+
+    // https://html.spec.whatwg.org/multipage/#dom-customelementregistry-whendefined
+    #[allow(unrooted_must_root)]
+    fn WhenDefined(&self, name: DOMString) -> Rc<Promise> {
+        let global_scope = self.window.upcast::<GlobalScope>();
+
+        // Step 1
+        if !is_valid_custom_element_name(&name) {
+            let promise = Promise::new(global_scope);
+            promise.reject_native(global_scope.get_cx(), &DOMException::new(global_scope, DOMErrorName::SyntaxError));
+            return promise
+        }
+
+        // Step 2
+        if self.definitions.borrow().contains_key(&name) {
+            let promise = Promise::new(global_scope);
+            promise.resolve_native(global_scope.get_cx(), &UndefinedValue());
+            return promise
+        }
+
+        // Step 3
+        let mut map = self.when_defined.borrow_mut();
+
+        // Steps 4, 5
+        let promise = map.get(&name).cloned().unwrap_or_else(|| {
+            let promise = Promise::new(global_scope);
+            map.insert(name, promise.clone());
+            promise
+        });
+
+        // Step 6
+        promise
+    }
+}
+
+#[derive(HeapSizeOf, JSTraceable)]
+struct CustomElementDefinition {
+    name: DOMString,
+
+    local_name: DOMString,
+
+    #[ignore_heap_size_of = "Rc"]
+    constructor: Rc<Function>,
+}
+
+impl CustomElementDefinition {
+    fn new(name: DOMString, local_name: DOMString, constructor: Rc<Function>) -> CustomElementDefinition {
+        CustomElementDefinition {
+            name: name,
+            local_name: local_name,
+            constructor: constructor,
+        }
+    }
+}
+
+// https://html.spec.whatwg.org/multipage/#valid-custom-element-name
+fn is_valid_custom_element_name(name: &str) -> bool {
+    // Custom elment names must match:
+    // PotentialCustomElementName ::= [a-z] (PCENChar)* '-' (PCENChar)*
+
+    let mut chars = name.chars();
+    if !chars.next().map_or(false, |c| c >= 'a' && c <= 'z') {
+        return false;
+    }
+
+    let mut has_dash = false;
+
+    for c in chars {
+        if c == '-' {
+            has_dash = true;
+            continue;
+        }
+
+        if !is_potential_custom_element_char(c) {
+            return false;
+        }
+    }
+
+    if !has_dash {
+        return false;
+    }
+
+    if name == "annotation-xml" ||
+        name == "color-profile" ||
+        name == "font-face" ||
+        name == "font-face-src" ||
+        name == "font-face-uri" ||
+        name == "font-face-format" ||
+        name == "font-face-name" ||
+        name == "missing-glyph"
+    {
+        return false;
+    }
+
+    true
+}
+
+// Check if this character is a PCENChar
+// https://html.spec.whatwg.org/multipage/#prod-pcenchar
+fn is_potential_custom_element_char(c: char) -> bool {
+    c == '-' || c == '.' || c == '_' || c == '\u{B7}' ||
+    (c >= '0' && c <= '9') ||
+    (c >= 'a' && c <= 'z') ||
+    (c >= '\u{C0}' && c <= '\u{D6}') ||
+    (c >= '\u{D8}' && c <= '\u{F6}') ||
+    (c >= '\u{F8}' && c <= '\u{37D}') ||
+    (c >= '\u{37F}' && c <= '\u{1FFF}') ||
+    (c >= '\u{200C}' && c <= '\u{200D}') ||
+    (c >= '\u{203F}' && c <= '\u{2040}') ||
+    (c >= '\u{2070}' && c <= '\u{2FEF}') ||
+    (c >= '\u{3001}' && c <= '\u{D7FF}') ||
+    (c >= '\u{F900}' && c <= '\u{FDCF}') ||
+    (c >= '\u{FDF0}' && c <= '\u{FFFD}') ||
+    (c >= '\u{10000}' && c <= '\u{EFFFF}')
+}
+
+fn is_known_element_interface(element: &str) -> bool {
+    element == "a" ||
+    element == "abbr" ||
+    element == "acronym" ||
+    element == "address" ||
+    element == "applet" ||
+    element == "area" ||
+    element == "article" ||
+    element == "aside" ||
+    element == "audio" ||
+    element == "b" ||
+    element == "base" ||
+    element == "bdi" ||
+    element == "bdo" ||
+    element == "big" ||
+    element == "blockquote" ||
+    element == "body" ||
+    element == "br" ||
+    element == "button" ||
+    element == "canvas" ||
+    element == "caption" ||
+    element == "center" ||
+    element == "cite" ||
+    element == "code" ||
+    element == "col" ||
+    element == "colgroup" ||
+    element == "data" ||
+    element == "datalist" ||
+    element == "dd" ||
+    element == "del" ||
+    element == "details" ||
+    element == "dfn" ||
+    element == "dialog" ||
+    element == "dir" ||
+    element == "div" ||
+    element == "dl" ||
+    element == "dt" ||
+    element == "em" ||
+    element == "embed" ||
+    element == "fieldset" ||
+    element == "figcaption" ||
+    element == "figure" ||
+    element == "font" ||
+    element == "footer" ||
+    element == "form" ||
+    element == "frame" ||
+    element == "frameset" ||
+    element == "h1" ||
+    element == "h2" ||
+    element == "h3" ||
+    element == "h4" ||
+    element == "h5" ||
+    element == "h6" ||
+    element == "head" ||
+    element == "header" ||
+    element == "hgroup" ||
+    element == "hr" ||
+    element == "html" ||
+    element == "i" ||
+    element == "iframe" ||
+    element == "img" ||
+    element == "input" ||
+    element == "ins" ||
+    element == "kbd" ||
+    element == "label" ||
+    element == "legend" ||
+    element == "li" ||
+    element == "link" ||
+    element == "listing" ||
+    element == "main" ||
+    element == "map" ||
+    element == "mark" ||
+    element == "marquee" ||
+    element == "meta" ||
+    element == "meter" ||
+    element == "nav" ||
+    element == "nobr" ||
+    element == "noframes" ||
+    element == "noscript" ||
+    element == "object" ||
+    element == "ol" ||
+    element == "optgroup" ||
+    element == "option" ||
+    element == "output" ||
+    element == "p" ||
+    element == "param" ||
+    element == "plaintext" ||
+    element == "pre" ||
+    element == "progress" ||
+    element == "q" ||
+    element == "rp" ||
+    element == "rt" ||
+    element == "ruby" ||
+    element == "s" ||
+    element == "samp" ||
+    element == "script" ||
+    element == "section" ||
+    element == "select" ||
+    element == "small" ||
+    element == "source" ||
+    element == "span" ||
+    element == "strike" ||
+    element == "strong" ||
+    element == "style" ||
+    element == "sub" ||
+    element == "summary" ||
+    element == "sup" ||
+    element == "table" ||
+    element == "tbody" ||
+    element == "td" ||
+    element == "template" ||
+    element == "textarea" ||
+    element == "tfoot" ||
+    element == "th" ||
+    element == "thead" ||
+    element == "time" ||
+    element == "title" ||
+    element == "tr" ||
+    element == "tt" ||
+    element == "track" ||
+    element == "u" ||
+    element == "ul" ||
+    element == "var" ||
+    element == "video" ||
+    element == "wbr" ||
+    element == "xmp"
+}
--- a/servo/components/script/dom/mod.rs
+++ b/servo/components/script/dom/mod.rs
@@ -250,16 +250,17 @@ pub mod cssmediarule;
 pub mod cssnamespacerule;
 pub mod cssrule;
 pub mod cssrulelist;
 pub mod cssstyledeclaration;
 pub mod cssstylerule;
 pub mod cssstylesheet;
 pub mod csssupportsrule;
 pub mod cssviewportrule;
+pub mod customelementregistry;
 pub mod customevent;
 pub mod dedicatedworkerglobalscope;
 pub mod dissimilaroriginlocation;
 pub mod dissimilaroriginwindow;
 pub mod document;
 pub mod documentfragment;
 pub mod documenttype;
 pub mod domexception;
new file mode 100644
--- /dev/null
+++ b/servo/components/script/dom/webidls/CustomElementRegistry.webidl
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// https://html.spec.whatwg.org/multipage/#customelementregistry
+[Pref="dom.customelements.enabled"]
+interface CustomElementRegistry {
+  [Throws/*, CEReactions */]
+  void define(DOMString name, Function constructor_, optional ElementDefinitionOptions options);
+
+  any get(DOMString name);
+
+  Promise<void> whenDefined(DOMString name);
+};
+
+dictionary ElementDefinitionOptions {
+  DOMString extends;
+};
--- a/servo/components/script/dom/webidls/Window.webidl
+++ b/servo/components/script/dom/webidls/Window.webidl
@@ -10,16 +10,18 @@
   [BinaryName="Self_", Replaceable] readonly attribute WindowProxy self;
   [Unforgeable] readonly attribute Document document;
 
   // https://github.com/servo/servo/issues/14453
   // attribute DOMString name;
 
   [/*PutForwards=href, */Unforgeable] readonly attribute Location location;
   readonly attribute History history;
+  [Pref="dom.customelements.enabled"]
+  readonly attribute CustomElementRegistry customElements;
   //[Replaceable] readonly attribute BarProp locationbar;
   //[Replaceable] readonly attribute BarProp menubar;
   //[Replaceable] readonly attribute BarProp personalbar;
   //[Replaceable] readonly attribute BarProp scrollbars;
   //[Replaceable] readonly attribute BarProp statusbar;
   //[Replaceable] readonly attribute BarProp toolbar;
   attribute DOMString status;
   void close();
--- a/servo/components/script/dom/window.rs
+++ b/servo/components/script/dom/window.rs
@@ -27,16 +27,17 @@ use dom::bindings::refcounted::Trusted;
 use dom::bindings::reflector::DomObject;
 use dom::bindings::str::DOMString;
 use dom::bindings::structuredclone::StructuredCloneData;
 use dom::bindings::trace::RootedTraceableBox;
 use dom::bindings::utils::{GlobalStaticData, WindowProxyHandler};
 use dom::bluetooth::BluetoothExtraPermissionData;
 use dom::crypto::Crypto;
 use dom::cssstyledeclaration::{CSSModificationAccess, CSSStyleDeclaration, CSSStyleOwner};
+use dom::customelementregistry::CustomElementRegistry;
 use dom::document::{AnimationFrameCallback, Document};
 use dom::element::Element;
 use dom::event::Event;
 use dom::globalscope::GlobalScope;
 use dom::history::History;
 use dom::htmliframeelement::build_mozbrowser_custom_event;
 use dom::location::Location;
 use dom::mediaquerylist::{MediaQueryList, WeakMediaQueryListVec};
@@ -174,16 +175,17 @@ pub struct Window {
     navigator: MutNullableJS<Navigator>,
     #[ignore_heap_size_of = "Arc"]
     image_cache: Arc<ImageCache>,
     #[ignore_heap_size_of = "channels are hard"]
     image_cache_chan: Sender<ImageCacheMsg>,
     window_proxy: MutNullableJS<WindowProxy>,
     document: MutNullableJS<Document>,
     history: MutNullableJS<History>,
+    custom_element_registry: MutNullableJS<CustomElementRegistry>,
     performance: MutNullableJS<Performance>,
     navigation_start: u64,
     navigation_start_precise: f64,
     screen: MutNullableJS<Screen>,
     session_storage: MutNullableJS<Storage>,
     local_storage: MutNullableJS<Storage>,
     status: DOMRefCell<DOMString>,
 
@@ -528,16 +530,21 @@ impl WindowMethods for Window {
         self.document.get().expect("Document accessed before initialization.")
     }
 
     // https://html.spec.whatwg.org/multipage/#dom-history
     fn History(&self) -> Root<History> {
         self.history.or_init(|| History::new(self))
     }
 
+    // https://html.spec.whatwg.org/multipage/#dom-window-customelements
+    fn CustomElements(&self) -> Root<CustomElementRegistry> {
+        self.custom_element_registry.or_init(|| CustomElementRegistry::new(self))
+    }
+
     // https://html.spec.whatwg.org/multipage/#dom-location
     fn Location(&self) -> Root<Location> {
         self.Document().GetLocation().unwrap()
     }
 
     // https://html.spec.whatwg.org/multipage/#dom-sessionstorage
     fn SessionStorage(&self) -> Root<Storage> {
         self.session_storage.or_init(|| Storage::new(self, StorageType::Session))
@@ -1026,16 +1033,22 @@ impl Window {
     }
 
     pub fn clear_js_runtime(&self) {
         // We tear down the active document, which causes all the attached
         // nodes to dispose of their layout data. This messages the layout
         // thread, informing it that it can safely free the memory.
         self.Document().upcast::<Node>().teardown();
 
+        // Clean up any active promises
+        // https://github.com/servo/servo/issues/15318
+        if let Some(custom_elements) = self.custom_element_registry.get() {
+            custom_elements.teardown();
+        }
+
         // The above code may not catch all DOM objects (e.g. DOM
         // objects removed from the tree that haven't been collected
         // yet). There should not be any such DOM nodes with layout
         // data, but if there are, then when they are dropped, they
         // will attempt to send a message to the closed layout thread.
         // This causes memory safety issues, because the DOM node uses
         // the layout channel from its window, and the window has
         // already been GC'd.  For nodes which do not have a live
@@ -1800,16 +1813,17 @@ impl Window {
             user_interaction_task_source: user_task_source,
             networking_task_source: network_task_source,
             history_traversal_task_source: history_task_source,
             file_reading_task_source: file_task_source,
             image_cache_chan: image_cache_chan,
             image_cache: image_cache.clone(),
             navigator: Default::default(),
             history: Default::default(),
+            custom_element_registry: Default::default(),
             window_proxy: Default::default(),
             document: Default::default(),
             performance: Default::default(),
             navigation_start: (current_time.sec * 1000 + current_time.nsec as i64 / 1000000) as u64,
             navigation_start_precise: time::precise_time_ns() as f64,
             screen: Default::default(),
             session_storage: Default::default(),
             local_storage: Default::default(),
--- a/servo/resources/prefs.json
+++ b/servo/resources/prefs.json
@@ -1,11 +1,12 @@
 {
   "dom.bluetooth.enabled": false,
   "dom.bluetooth.testing.enabled": false,
+  "dom.customelements.enabled": false,
   "dom.forcetouch.enabled": false,
   "dom.gamepad.enabled": false,
   "dom.mouseevent.which.enabled": false,
   "dom.mozbrowser.enabled": false,
   "dom.mutation_observer.enabled": false,
   "dom.permissions.enabled": false,
   "dom.permissions.testing.allowed_in_nonsecure_contexts": false,
   "dom.serviceworker.timeout_seconds": 60,