From e21e64a33c6fa22342e006c08d0d41ad527dd9b8 Mon Sep 17 00:00:00 2001 From: Connor Brewster Date: Thu, 11 May 2017 12:47:27 -0600 Subject: [PATCH] Add custom element registry --- .../script/dom/customelementregistry.rs | 432 ++++++++++++++++++ components/script/dom/mod.rs | 1 + .../dom/webidls/CustomElementRegistry.webidl | 18 + components/script/dom/webidls/Window.webidl | 2 + components/script/dom/window.rs | 14 + resources/prefs.json | 1 + .../wpt/metadata/custom-elements/__dir__.ini | 1 + 7 files changed, 469 insertions(+) create mode 100644 components/script/dom/customelementregistry.rs create mode 100644 components/script/dom/webidls/CustomElementRegistry.webidl create mode 100644 tests/wpt/metadata/custom-elements/__dir__.ini diff --git a/components/script/dom/customelementregistry.rs b/components/script/dom/customelementregistry.rs new file mode 100644 index 00000000000..eaf7782cb5c --- /dev/null +++ b/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, + + #[ignore_heap_size_of = "Rc"] + when_defined: DOMRefCell>>, + + element_definition_is_running: Cell, + + definitions: DOMRefCell>, +} + +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 { + 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::(); + 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, options: &ElementDefinitionOptions) -> ErrorResult { + let global_scope = self.window.upcast::(); + 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 { + let global_scope = self.window.upcast::(); + + // 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, +} + +impl CustomElementDefinition { + fn new(name: DOMString, local_name: DOMString, constructor: Rc) -> 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" +} diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs index bb5f5bf1a71..8ab41391bb2 100644 --- a/components/script/dom/mod.rs +++ b/components/script/dom/mod.rs @@ -255,6 +255,7 @@ pub mod cssstylerule; pub mod cssstylesheet; pub mod csssupportsrule; pub mod cssviewportrule; +pub mod customelementregistry; pub mod customevent; pub mod dedicatedworkerglobalscope; pub mod dissimilaroriginlocation; diff --git a/components/script/dom/webidls/CustomElementRegistry.webidl b/components/script/dom/webidls/CustomElementRegistry.webidl new file mode 100644 index 00000000000..263726e1e47 --- /dev/null +++ b/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 whenDefined(DOMString name); +}; + +dictionary ElementDefinitionOptions { + DOMString extends; +}; diff --git a/components/script/dom/webidls/Window.webidl b/components/script/dom/webidls/Window.webidl index 3c962f2795e..c4c25399743 100644 --- a/components/script/dom/webidls/Window.webidl +++ b/components/script/dom/webidls/Window.webidl @@ -15,6 +15,8 @@ [/*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; diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index b4459bf15db..fc793c206c4 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -32,6 +32,7 @@ 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; @@ -179,6 +180,7 @@ pub struct Window { window_proxy: MutNullableJS, document: MutNullableJS, history: MutNullableJS, + custom_element_registry: MutNullableJS, performance: MutNullableJS, navigation_start: u64, navigation_start_precise: f64, @@ -533,6 +535,11 @@ impl WindowMethods for Window { self.history.or_init(|| History::new(self)) } + // https://html.spec.whatwg.org/multipage/#dom-window-customelements + fn CustomElements(&self) -> Root { + self.custom_element_registry.or_init(|| CustomElementRegistry::new(self)) + } + // https://html.spec.whatwg.org/multipage/#dom-location fn Location(&self) -> Root { self.Document().GetLocation().unwrap() @@ -1031,6 +1038,12 @@ impl Window { // thread, informing it that it can safely free the memory. self.Document().upcast::().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 @@ -1805,6 +1818,7 @@ impl Window { 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(), diff --git a/resources/prefs.json b/resources/prefs.json index 0bd519b2557..32406257d61 100644 --- a/resources/prefs.json +++ b/resources/prefs.json @@ -1,6 +1,7 @@ { "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, diff --git a/tests/wpt/metadata/custom-elements/__dir__.ini b/tests/wpt/metadata/custom-elements/__dir__.ini new file mode 100644 index 00000000000..2439be8f9ce --- /dev/null +++ b/tests/wpt/metadata/custom-elements/__dir__.ini @@ -0,0 +1 @@ +prefs: [dom.customelements.enabled:true]