Auto merge of #17112 - cbrewster:custom_element_registry, r=jdm

Implement custom element registry

<!-- 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. -->

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/17112)
<!-- Reviewable:end -->
This commit is contained in:
bors-servo 2017-06-05 08:38:03 -07:00 committed by GitHub
commit b584944f17
13 changed files with 520 additions and 87 deletions

View file

@ -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"
}

View file

@ -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;

View file

@ -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;
};

View file

@ -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;

View file

@ -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<WindowProxy>,
document: MutNullableJS<Document>,
history: MutNullableJS<History>,
custom_element_registry: MutNullableJS<CustomElementRegistry>,
performance: MutNullableJS<Performance>,
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<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()
@ -1031,6 +1038,12 @@ impl Window {
// 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
@ -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(),

View file

@ -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,

View file

@ -554144,7 +554144,7 @@
"testharness"
],
"custom-elements/custom-element-registry/define.html": [
"724060b276a1f3254fd226e329631005ec5a5e87",
"4f3d4da1aae8efab035a4b951147904d71e7de3b",
"testharness"
],
"custom-elements/disconnected-callbacks.html": [

View file

@ -1,50 +1,8 @@
[CustomElementRegistry.html]
type: testharness
[CustomElementRegistry interface must have define as a method]
expected: FAIL
[customElements.define must throw when the element interface is not a constructor]
expected: FAIL
[customElements.define must not throw the constructor is HTMLElement]
expected: FAIL
[customElements.define must throw with an invalid name]
expected: FAIL
[customElements.define must throw when there is already a custom element of the same name]
expected: FAIL
[customElements.define must throw a NotSupportedError when there is already a custom element with the same class]
expected: FAIL
[customElements.define must throw a NotSupportedError when element definition is running flag is set]
expected: FAIL
[customElements.define must check IsConstructor on the constructor before checking the element definition is running flag]
expected: FAIL
[customElements.define must validate the custom element name before checking the element definition is running flag]
expected: FAIL
[customElements.define unset the element definition is running flag before upgrading custom elements]
expected: FAIL
[customElements.define must not throw when defining another custom element in a different global object during Get(constructor, "prototype")]
expected: FAIL
[Custom Elements: CustomElementRegistry interface]
expected: FAIL
[customElements.define must get "prototype" property of the constructor]
expected: FAIL
[customElements.define must rethrow an exception thrown while getting "prototype" property of the constructor]
expected: FAIL
[customElements.define must throw when "prototype" property of the constructor is not an object]
expected: FAIL
[customElements.define must get callbacks of the constructor prototype]
expected: FAIL
@ -69,45 +27,9 @@
[customElements.define must rethrow an exception thrown while retrieving Symbol.iterator on observedAttributes]
expected: FAIL
[customElements.define must not throw even if "observedAttributes" fails to convert if "attributeChangedCallback" is not defined]
expected: FAIL
[customElements.define must define an instantiatable custom element]
expected: FAIL
[customElements.define must upgrade elements in the shadow-including tree order]
expected: FAIL
[CustomElementRegistry interface must have get as a method]
expected: FAIL
[customElements.get must return undefined when the registry does not contain an entry with the given name]
expected: FAIL
[customElements.get must return undefined when the registry does not contain an entry with the given name even if the name was not a valid custom element name]
expected: FAIL
[customElements.get return the constructor of the entry with the given name when there is a matching entry.]
expected: FAIL
[customElements.whenDefined must return a promise for a valid custom element name]
expected: FAIL
[customElements.whenDefined must return the same promise each time invoked for a valid custom element name which has not been defined]
expected: FAIL
[customElements.whenDefined must return an unresolved promise when the registry does not contain the entry with the given name]
expected: FAIL
[customElements.whenDefined must return a rejected promise when the given name is not a valid custom element name]
expected: FAIL
[customElements.whenDefined must return a resolved promise when the registry contains the entry with the given name]
expected: FAIL
[customElements.whenDefined must return a new resolved promise each time invoked when the registry contains the entry with the given name]
expected: FAIL
[A promise returned by customElements.whenDefined must be resolved by "define"]
expected: FAIL

View file

@ -9,9 +9,3 @@
[HTMLElement constructor must allow subclassing an user-defined subclass of HTMLElement]
expected: FAIL
[HTMLElement constructor must throw a TypeError when NewTarget is equal to itself]
expected: FAIL
[HTMLElement constructor must throw a TypeError when NewTarget is equal to itself via a Proxy object]
expected: FAIL

View file

@ -0,0 +1 @@
prefs: [dom.customelements.enabled:true]

View file

@ -1,5 +1,53 @@
[define.html]
type: testharness
[Custom Elements: Element definition]
[If constructor is arrow function, should throw a TypeError]
expected: FAIL
[If constructor.prototype.connectedCallback throws, should rethrow]
expected: FAIL
[If constructor.prototype.connectedCallback is null, should throw a TypeError]
expected: FAIL
[If constructor.prototype.connectedCallback is object, should throw a TypeError]
expected: FAIL
[If constructor.prototype.connectedCallback is integer, should throw a TypeError]
expected: FAIL
[If constructor.prototype.disconnectedCallback throws, should rethrow]
expected: FAIL
[If constructor.prototype.disconnectedCallback is null, should throw a TypeError]
expected: FAIL
[If constructor.prototype.disconnectedCallback is object, should throw a TypeError]
expected: FAIL
[If constructor.prototype.disconnectedCallback is integer, should throw a TypeError]
expected: FAIL
[If constructor.prototype.adoptedCallback throws, should rethrow]
expected: FAIL
[If constructor.prototype.adoptedCallback is null, should throw a TypeError]
expected: FAIL
[If constructor.prototype.adoptedCallback is object, should throw a TypeError]
expected: FAIL
[If constructor.prototype.adoptedCallback is integer, should throw a TypeError]
expected: FAIL
[If constructor.prototype.attributeChangedCallback throws, should rethrow]
expected: FAIL
[If constructor.prototype.attributeChangedCallback is null, should throw a TypeError]
expected: FAIL
[If constructor.prototype.attributeChangedCallback is object, should throw a TypeError]
expected: FAIL
[If constructor.prototype.attributeChangedCallback is integer, should throw a TypeError]
expected: FAIL

View file

@ -1,6 +1,5 @@
[parser-constructs-custom-elements.html]
type: testharness
expected: ERROR
[HTML parser must create a defined custom element before executing inline scripts]
expected: FAIL

View file

@ -12,6 +12,7 @@
// https://html.spec.whatwg.org/multipage/scripting.html#element-definition
// Use window from iframe to isolate the test.
const iframe = document.getElementById("iframe");
const testWindow = iframe.contentDocument.defaultView;
const customElements = testWindow.customElements;