mirror of
https://github.com/servo/servo.git
synced 2025-06-08 00:23:30 +00:00
This also adds comments for missing steps. The only change in behaviour should be the debug message when an external script fails to load.
509 lines
18 KiB
Rust
509 lines
18 KiB
Rust
/* 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 std::ascii::AsciiExt;
|
|
|
|
use dom::attr::Attr;
|
|
use dom::attr::AttrHelpers;
|
|
use dom::bindings::codegen::Bindings::AttrBinding::AttrMethods;
|
|
use dom::bindings::codegen::Bindings::HTMLScriptElementBinding;
|
|
use dom::bindings::codegen::Bindings::HTMLScriptElementBinding::HTMLScriptElementMethods;
|
|
use dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
|
|
use dom::bindings::codegen::InheritTypes::{HTMLScriptElementDerived, HTMLScriptElementCast};
|
|
use dom::bindings::codegen::InheritTypes::{ElementCast, HTMLElementCast, NodeCast};
|
|
use dom::bindings::codegen::InheritTypes::EventTargetCast;
|
|
use dom::bindings::global::GlobalRef;
|
|
use dom::bindings::js::{JSRef, Temporary, OptionalRootable};
|
|
use dom::bindings::refcounted::Trusted;
|
|
use dom::document::Document;
|
|
use dom::element::{Element, AttributeHandlers, ElementCreator};
|
|
use dom::eventtarget::{EventTarget, EventTargetTypeId};
|
|
use dom::event::{Event, EventBubbles, EventCancelable, EventHelpers};
|
|
use dom::element::ElementTypeId;
|
|
use dom::htmlelement::{HTMLElement, HTMLElementTypeId};
|
|
use dom::node::{Node, NodeHelpers, NodeTypeId, window_from_node, CloneChildrenFlag};
|
|
use dom::virtualmethods::VirtualMethods;
|
|
use dom::window::ScriptHelpers;
|
|
use script_task::{ScriptMsg, Runnable};
|
|
|
|
use encoding::all::UTF_8;
|
|
use encoding::types::{Encoding, DecoderTrap};
|
|
use net::resource_task::{load_whole_resource, Metadata};
|
|
use util::str::{DOMString, HTML_SPACE_CHARACTERS, StaticStringVec};
|
|
use std::borrow::ToOwned;
|
|
use std::cell::Cell;
|
|
use string_cache::Atom;
|
|
use url::{Url, UrlParser};
|
|
|
|
#[dom_struct]
|
|
pub struct HTMLScriptElement {
|
|
htmlelement: HTMLElement,
|
|
|
|
/// https://html.spec.whatwg.org/multipage/scripting.html#already-started
|
|
already_started: Cell<bool>,
|
|
|
|
/// https://html.spec.whatwg.org/multipage/scripting.html#parser-inserted
|
|
parser_inserted: Cell<bool>,
|
|
|
|
/// https://html.spec.whatwg.org/multipage/scripting.html#non-blocking
|
|
///
|
|
/// (currently unused)
|
|
non_blocking: Cell<bool>,
|
|
|
|
/// https://html.spec.whatwg.org/multipage/scripting.html#ready-to-be-parser-executed
|
|
///
|
|
/// (currently unused)
|
|
ready_to_be_parser_executed: Cell<bool>,
|
|
}
|
|
|
|
impl HTMLScriptElementDerived for EventTarget {
|
|
fn is_htmlscriptelement(&self) -> bool {
|
|
*self.type_id() == EventTargetTypeId::Node(NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLScriptElement)))
|
|
}
|
|
}
|
|
|
|
impl HTMLScriptElement {
|
|
fn new_inherited(localName: DOMString, prefix: Option<DOMString>, document: JSRef<Document>,
|
|
creator: ElementCreator) -> HTMLScriptElement {
|
|
HTMLScriptElement {
|
|
htmlelement: HTMLElement::new_inherited(HTMLElementTypeId::HTMLScriptElement, localName, prefix, document),
|
|
already_started: Cell::new(false),
|
|
parser_inserted: Cell::new(creator == ElementCreator::ParserCreated),
|
|
non_blocking: Cell::new(creator != ElementCreator::ParserCreated),
|
|
ready_to_be_parser_executed: Cell::new(false),
|
|
}
|
|
}
|
|
|
|
#[allow(unrooted_must_root)]
|
|
pub fn new(localName: DOMString, prefix: Option<DOMString>, document: JSRef<Document>,
|
|
creator: ElementCreator) -> Temporary<HTMLScriptElement> {
|
|
let element = HTMLScriptElement::new_inherited(localName, prefix, document, creator);
|
|
Node::reflect_node(box element, document, HTMLScriptElementBinding::Wrap)
|
|
}
|
|
}
|
|
|
|
pub trait HTMLScriptElementHelpers {
|
|
/// Prepare a script (<http://www.whatwg.org/html/#prepare-a-script>)
|
|
fn prepare(self);
|
|
|
|
/// [Execute a script block]
|
|
/// (https://html.spec.whatwg.org/multipage/#execute-the-script-block)
|
|
fn execute(self, load: ScriptOrigin);
|
|
|
|
/// Prepare a script, steps 6 and 7.
|
|
fn is_javascript(self) -> bool;
|
|
|
|
/// Set the "already started" flag (<https://whatwg.org/html/#already-started>)
|
|
fn mark_already_started(self);
|
|
|
|
// Queues error event
|
|
fn queue_error_event(self);
|
|
|
|
/// Dispatch load event.
|
|
fn dispatch_load_event(self);
|
|
|
|
/// Dispatch error event.
|
|
fn dispatch_error_event(self);
|
|
}
|
|
|
|
/// Supported script types as defined by
|
|
/// <http://whatwg.org/html/#support-the-scripting-language>.
|
|
static SCRIPT_JS_MIMES: StaticStringVec = &[
|
|
"application/ecmascript",
|
|
"application/javascript",
|
|
"application/x-ecmascript",
|
|
"application/x-javascript",
|
|
"text/ecmascript",
|
|
"text/javascript",
|
|
"text/javascript1.0",
|
|
"text/javascript1.1",
|
|
"text/javascript1.2",
|
|
"text/javascript1.3",
|
|
"text/javascript1.4",
|
|
"text/javascript1.5",
|
|
"text/jscript",
|
|
"text/livescript",
|
|
"text/x-ecmascript",
|
|
"text/x-javascript",
|
|
];
|
|
|
|
pub enum ScriptOrigin {
|
|
Internal(String, Url),
|
|
External(Result<(Metadata, Vec<u8>), String>),
|
|
}
|
|
|
|
impl<'a> HTMLScriptElementHelpers for JSRef<'a, HTMLScriptElement> {
|
|
fn prepare(self) {
|
|
// https://html.spec.whatwg.org/multipage/scripting.html#prepare-a-script
|
|
// Step 1.
|
|
if self.already_started.get() {
|
|
return;
|
|
}
|
|
// Step 2.
|
|
let was_parser_inserted = self.parser_inserted.get();
|
|
self.parser_inserted.set(false);
|
|
|
|
// Step 3.
|
|
let element: JSRef<Element> = ElementCast::from_ref(self);
|
|
if was_parser_inserted && element.has_attribute(&atom!("async")) {
|
|
self.non_blocking.set(true);
|
|
}
|
|
// Step 4.
|
|
let text = self.Text();
|
|
if text.len() == 0 && !element.has_attribute(&atom!("src")) {
|
|
return;
|
|
}
|
|
// Step 5.
|
|
let node: JSRef<Node> = NodeCast::from_ref(self);
|
|
if !node.is_in_doc() {
|
|
return;
|
|
}
|
|
// Step 6, 7.
|
|
if !self.is_javascript() {
|
|
return;
|
|
}
|
|
// Step 8.
|
|
if was_parser_inserted {
|
|
self.parser_inserted.set(true);
|
|
self.non_blocking.set(false);
|
|
}
|
|
// Step 9.
|
|
self.already_started.set(true);
|
|
|
|
// Step 10.
|
|
// TODO: If the element is flagged as "parser-inserted", but the element's node document is
|
|
// not the Document of the parser that created the element, then abort these steps.
|
|
|
|
// Step 11.
|
|
// TODO: If scripting is disabled for the script element, then the user agent must abort
|
|
// these steps at this point. The script is not executed.
|
|
|
|
// Step 12.
|
|
match element.get_attribute(ns!(""), &atom!("for")).root() {
|
|
Some(for_script) => {
|
|
if for_script.r().Value().to_ascii_lowercase().trim_matches(HTML_SPACE_CHARACTERS) != "window" {
|
|
return;
|
|
}
|
|
}
|
|
_ => { }
|
|
}
|
|
|
|
match element.get_attribute(ns!(""), &Atom::from_slice("event")).root() {
|
|
Some(event) => {
|
|
let event = event.r().Value().to_ascii_lowercase();
|
|
let event = event.trim_matches(HTML_SPACE_CHARACTERS);
|
|
if event != "onload" && event != "onload()" {
|
|
return;
|
|
}
|
|
}
|
|
_ => { }
|
|
}
|
|
// Step 13.
|
|
// TODO: If the script element has a `charset` attribute, then let the script block's
|
|
// character encoding for this script element be the result of getting an encoding from the
|
|
// value of the `charset` attribute.
|
|
|
|
// Step 14.
|
|
let window = window_from_node(self).root();
|
|
let window = window.r();
|
|
let page = window.page();
|
|
let base_url = page.get_url();
|
|
|
|
let load = match element.get_attribute(ns!(""), &atom!("src")).root() {
|
|
// Step 14.
|
|
Some(src) => {
|
|
// Step 14.1
|
|
let src = src.r().Value();
|
|
|
|
// Step 14.2
|
|
if src.is_empty() {
|
|
self.queue_error_event();
|
|
return;
|
|
}
|
|
|
|
// Step 14.3
|
|
match UrlParser::new().base_url(&base_url).parse(&*src) {
|
|
Err(_) => {
|
|
// Step 14.4
|
|
error!("error parsing URL for script {}", src);
|
|
self.queue_error_event();
|
|
return;
|
|
}
|
|
Ok(url) => {
|
|
// Step 14.5
|
|
// TODO: Do a potentially CORS-enabled fetch with the mode being the current
|
|
// state of the element's `crossorigin` content attribute, the origin being
|
|
// the origin of the script element's node document, and the default origin
|
|
// behaviour set to taint.
|
|
ScriptOrigin::External(load_whole_resource(&page.resource_task, url))
|
|
}
|
|
}
|
|
},
|
|
None => ScriptOrigin::Internal(text, base_url),
|
|
};
|
|
|
|
// Step 15.
|
|
// TODO: Add support for the `defer` and `async` attributes. (For now, we fetch all
|
|
// scripts synchronously and execute them immediately.)
|
|
self.execute(load);
|
|
}
|
|
|
|
fn execute(self, load: ScriptOrigin) {
|
|
// Step 1.
|
|
// TODO: If the element is flagged as "parser-inserted", but the
|
|
// element's node document is not the Document of the parser that
|
|
// created the element, then abort these steps.
|
|
|
|
// Step 2.
|
|
let (source, external, url) = match load {
|
|
// Step 2.a.
|
|
ScriptOrigin::External(Err(e)) => {
|
|
error!("error loading script {}", e);
|
|
self.queue_error_event();
|
|
return;
|
|
}
|
|
|
|
// Step 2.b.1.a.
|
|
ScriptOrigin::External(Ok((metadata, bytes))) => {
|
|
// Step 1.
|
|
// TODO: If the resource's Content Type metadata, if any,
|
|
// specifies a character encoding, and the user agent supports
|
|
// that encoding, then let character encoding be that encoding,
|
|
// and jump to the bottom step in this series of steps.
|
|
|
|
// Step 2.
|
|
// TODO: If the algorithm above set the script block's
|
|
// character encoding, then let character encoding be that
|
|
// encoding, and jump to the bottom step in this series of
|
|
// steps.
|
|
|
|
// Step 3.
|
|
// TODO: Let character encoding be the script block's fallback
|
|
// character encoding.
|
|
|
|
// Step 4.
|
|
// TODO: Otherwise, decode the file to Unicode, using character
|
|
// encoding as the fallback encoding.
|
|
|
|
(UTF_8.decode(&*bytes, DecoderTrap::Replace).unwrap(), true,
|
|
metadata.final_url)
|
|
},
|
|
|
|
// Step 2.b.1.c.
|
|
ScriptOrigin::Internal(text, url) => {
|
|
(text, false, url)
|
|
}
|
|
};
|
|
|
|
// Step 2.b.2.
|
|
// TODO: Fire a simple event named beforescriptexecute that bubbles and
|
|
// is cancelable at the script element.
|
|
// If the event is canceled, then abort these steps.
|
|
|
|
// Step 2.b.3.
|
|
// TODO: If the script is from an external file, then increment the
|
|
// ignore-destructive-writes counter of the script element's node
|
|
// document. Let neutralised doc be that Document.
|
|
|
|
// Step 2.b.4.
|
|
// TODO: Let old script element be the value to which the script
|
|
// element's node document's currentScript object was most recently
|
|
// initialised.
|
|
|
|
// Step 2.b.5.
|
|
// TODO: Initialise the script element's node document's currentScript
|
|
// object to the script element.
|
|
|
|
// Step 2.b.6.
|
|
// TODO: Create a script...
|
|
let window = window_from_node(self).root();
|
|
window.r().evaluate_script_on_global_with_result(&*source,
|
|
&*url.serialize());
|
|
|
|
// Step 2.b.7.
|
|
// TODO: Initialise the script element's node document's currentScript
|
|
// object to old script element.
|
|
|
|
// Step 2.b.8.
|
|
// TODO: Decrement the ignore-destructive-writes counter of neutralised
|
|
// doc, if it was incremented in the earlier step.
|
|
|
|
// Step 2.b.9.
|
|
// TODO: Fire a simple event named afterscriptexecute that bubbles (but
|
|
// is not cancelable) at the script element.
|
|
|
|
// Step 2.b.10.
|
|
if external {
|
|
self.dispatch_load_event();
|
|
} else {
|
|
let chan = window.r().script_chan();
|
|
let handler = Trusted::new(window.r().get_cx(), self, chan.clone());
|
|
let dispatcher = Box::new(EventDispatcher {
|
|
element: handler,
|
|
is_error: false,
|
|
});
|
|
chan.send(ScriptMsg::RunnableMsg(dispatcher)).unwrap();
|
|
}
|
|
}
|
|
|
|
fn queue_error_event(self) {
|
|
let window = window_from_node(self).root();
|
|
let window = window.r();
|
|
let chan = window.script_chan();
|
|
let handler = Trusted::new(window.get_cx(), self, chan.clone());
|
|
let dispatcher = Box::new(EventDispatcher {
|
|
element: handler,
|
|
is_error: true,
|
|
});
|
|
chan.send(ScriptMsg::RunnableMsg(dispatcher)).unwrap();
|
|
}
|
|
|
|
fn dispatch_load_event(self) {
|
|
let window = window_from_node(self).root();
|
|
let window = window.r();
|
|
let event = Event::new(GlobalRef::Window(window),
|
|
"load".to_owned(),
|
|
EventBubbles::DoesNotBubble,
|
|
EventCancelable::NotCancelable).root();
|
|
let target: JSRef<EventTarget> = EventTargetCast::from_ref(self);
|
|
event.r().fire(target);
|
|
}
|
|
|
|
fn dispatch_error_event(self) {
|
|
let window = window_from_node(self).root();
|
|
let window = window.r();
|
|
let event = Event::new(GlobalRef::Window(window),
|
|
"error".to_owned(),
|
|
EventBubbles::DoesNotBubble,
|
|
EventCancelable::NotCancelable).root();
|
|
let target: JSRef<EventTarget> = EventTargetCast::from_ref(self);
|
|
event.r().fire(target);
|
|
}
|
|
|
|
fn is_javascript(self) -> bool {
|
|
let element: JSRef<Element> = ElementCast::from_ref(self);
|
|
match element.get_attribute(ns!(""), &atom!("type")).root().map(|s| s.r().Value()) {
|
|
Some(ref s) if s.is_empty() => {
|
|
// type attr exists, but empty means js
|
|
debug!("script type empty, inferring js");
|
|
true
|
|
},
|
|
Some(ref s) => {
|
|
debug!("script type={}", *s);
|
|
SCRIPT_JS_MIMES.contains(&s.to_ascii_lowercase().as_slice().trim_matches(HTML_SPACE_CHARACTERS))
|
|
},
|
|
None => {
|
|
debug!("no script type");
|
|
match element.get_attribute(ns!(""), &atom!("language"))
|
|
.root()
|
|
.map(|s| s.r().Value()) {
|
|
Some(ref s) if s.is_empty() => {
|
|
debug!("script language empty, inferring js");
|
|
true
|
|
},
|
|
Some(ref s) => {
|
|
debug!("script language={}", *s);
|
|
SCRIPT_JS_MIMES.contains(&format!("text/{}", s).to_ascii_lowercase().as_slice())
|
|
},
|
|
None => {
|
|
debug!("no script type or language, inferring js");
|
|
true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn mark_already_started(self) {
|
|
self.already_started.set(true);
|
|
}
|
|
}
|
|
|
|
impl<'a> VirtualMethods for JSRef<'a, HTMLScriptElement> {
|
|
fn super_type<'b>(&'b self) -> Option<&'b VirtualMethods> {
|
|
let htmlelement: &JSRef<HTMLElement> = HTMLElementCast::from_borrowed_ref(self);
|
|
Some(htmlelement as &VirtualMethods)
|
|
}
|
|
|
|
fn after_set_attr(&self, attr: JSRef<Attr>) {
|
|
match self.super_type() {
|
|
Some(ref s) => s.after_set_attr(attr),
|
|
_ => (),
|
|
}
|
|
let node: JSRef<Node> = NodeCast::from_ref(*self);
|
|
if attr.local_name() == &atom!("src") && !self.parser_inserted.get() && node.is_in_doc() {
|
|
self.prepare();
|
|
}
|
|
}
|
|
|
|
fn child_inserted(&self, child: JSRef<Node>) {
|
|
match self.super_type() {
|
|
Some(ref s) => s.child_inserted(child),
|
|
_ => (),
|
|
}
|
|
let node: JSRef<Node> = NodeCast::from_ref(*self);
|
|
if !self.parser_inserted.get() && node.is_in_doc() {
|
|
self.prepare();
|
|
}
|
|
}
|
|
|
|
fn bind_to_tree(&self, tree_in_doc: bool) {
|
|
match self.super_type() {
|
|
Some(ref s) => s.bind_to_tree(tree_in_doc),
|
|
_ => ()
|
|
}
|
|
|
|
if tree_in_doc && !self.parser_inserted.get() {
|
|
self.prepare();
|
|
}
|
|
}
|
|
|
|
fn cloning_steps(&self, copy: JSRef<Node>, maybe_doc: Option<JSRef<Document>>,
|
|
clone_children: CloneChildrenFlag) {
|
|
match self.super_type() {
|
|
Some(ref s) => s.cloning_steps(copy, maybe_doc, clone_children),
|
|
_ => (),
|
|
}
|
|
|
|
// https://whatwg.org/html/#already-started
|
|
if self.already_started.get() {
|
|
let copy_elem: JSRef<HTMLScriptElement> = HTMLScriptElementCast::to_ref(copy).unwrap();
|
|
copy_elem.mark_already_started();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> HTMLScriptElementMethods for JSRef<'a, HTMLScriptElement> {
|
|
make_url_getter!(Src);
|
|
|
|
make_setter!(SetSrc, "src");
|
|
|
|
// http://www.whatwg.org/html/#dom-script-text
|
|
fn Text(self) -> DOMString {
|
|
let node: JSRef<Node> = NodeCast::from_ref(self);
|
|
Node::collect_text_contents(node.children())
|
|
}
|
|
|
|
// http://www.whatwg.org/html/#dom-script-text
|
|
fn SetText(self, value: DOMString) {
|
|
let node: JSRef<Node> = NodeCast::from_ref(self);
|
|
node.SetTextContent(Some(value))
|
|
}
|
|
}
|
|
|
|
struct EventDispatcher {
|
|
element: Trusted<HTMLScriptElement>,
|
|
is_error: bool,
|
|
}
|
|
|
|
impl Runnable for EventDispatcher {
|
|
fn handler(self: Box<EventDispatcher>) {
|
|
let target = self.element.to_temporary().root();
|
|
if self.is_error {
|
|
target.r().dispatch_error_event();
|
|
} else {
|
|
target.r().dispatch_load_event();
|
|
}
|
|
}
|
|
}
|