Auto merge of #14361 - nox:write, r=jdm

Implement document.write (fixes #3704)

<!-- 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/14361)
<!-- Reviewable:end -->
This commit is contained in:
bors-servo 2016-11-29 04:18:43 -08:00 committed by GitHub
commit 00f30d1b7e
61 changed files with 369 additions and 363 deletions

View file

@ -142,12 +142,6 @@ pub enum IsHTMLDocument {
NonHTMLDocument,
}
#[derive(PartialEq)]
enum ParserBlockedByScript {
Blocked,
Unblocked,
}
#[derive(JSTraceable, HeapSizeOf)]
#[must_root]
pub struct StylesheetInDocument {
@ -287,6 +281,8 @@ pub struct Document {
/// https://w3c.github.io/uievents/#event-type-dblclick
#[ignore_heap_size_of = "Defined in std"]
last_click_info: DOMRefCell<Option<(Instant, Point2D<f32>)>>,
/// https://html.spec.whatwg.org/multipage/#ignore-destructive-writes-counter
ignore_destructive_writes_counter: Cell<u32>,
}
#[derive(JSTraceable, HeapSizeOf)]
@ -378,15 +374,16 @@ impl Document {
self.trigger_mozbrowser_event(MozBrowserEvent::SecurityChange(https_state));
}
// https://html.spec.whatwg.org/multipage/#active-document
pub fn is_active(&self) -> bool {
self.browsing_context().map_or(false, |context| {
self == &*context.active_document()
})
}
// https://html.spec.whatwg.org/multipage/#fully-active
pub fn is_fully_active(&self) -> bool {
let browsing_context = match self.browsing_context() {
Some(browsing_context) => browsing_context,
None => return false,
};
let active_document = browsing_context.active_document();
if self != &*active_document {
if !self.is_active() {
return false;
}
// FIXME: It should also check whether the browser context is top-level or not
@ -1545,15 +1542,13 @@ impl Document {
self.process_asap_scripts();
}
if self.maybe_execute_parser_blocking_script() == ParserBlockedByScript::Blocked {
return;
}
// A finished resource load can potentially unblock parsing. In that case, resume the
// parser so its loop can find out.
if let Some(parser) = self.get_current_parser() {
if parser.is_suspended() {
parser.resume();
if let Some(script) = self.pending_parsing_blocking_script.get() {
if self.script_blocking_stylesheets_count.get() > 0 || !script.is_ready_to_be_executed() {
return;
}
self.pending_parsing_blocking_script.set(None);
parser.resume_with_pending_parsing_blocking_script(&script);
}
} else if self.reflow_timeout.get().is_none() {
// If we don't have a parser, and the reflow timer has been reset, explicitly
@ -1576,23 +1571,6 @@ impl Document {
}
}
/// If document parsing is blocked on a script, and that script is ready to run,
/// execute it.
/// https://html.spec.whatwg.org/multipage/#ready-to-be-parser-executed
fn maybe_execute_parser_blocking_script(&self) -> ParserBlockedByScript {
let script = match self.pending_parsing_blocking_script.get() {
None => return ParserBlockedByScript::Unblocked,
Some(script) => script,
};
if self.script_blocking_stylesheets_count.get() == 0 && script.is_ready_to_be_executed() {
self.pending_parsing_blocking_script.set(None);
script.execute();
return ParserBlockedByScript::Unblocked;
}
ParserBlockedByScript::Blocked
}
/// https://html.spec.whatwg.org/multipage/#the-end step 3
pub fn process_deferred_scripts(&self) {
if self.ready_state.get() != DocumentReadyState::Interactive {
@ -1901,6 +1879,7 @@ impl Document {
referrer_policy: Cell::new(referrer_policy),
target_element: MutNullableHeap::new(None),
last_click_info: DOMRefCell::new(None),
ignore_destructive_writes_counter: Default::default(),
}
}
@ -2077,6 +2056,16 @@ impl Document {
ReflowQueryType::NoQuery,
ReflowReason::ElementStateChanged);
}
pub fn incr_ignore_destructive_writes_counter(&self) {
self.ignore_destructive_writes_counter.set(
self.ignore_destructive_writes_counter.get() + 1);
}
pub fn decr_ignore_destructive_writes_counter(&self) {
self.ignore_destructive_writes_counter.set(
self.ignore_destructive_writes_counter.get() - 1);
}
}
@ -3043,6 +3032,55 @@ impl DocumentMethods for Document {
elements
}
// https://html.spec.whatwg.org/multipage/#dom-document-write
fn Write(&self, text: Vec<DOMString>) -> ErrorResult {
if !self.is_html_document() {
// Step 1.
return Err(Error::InvalidState);
}
// Step 2.
// TODO: handle throw-on-dynamic-markup-insertion counter.
if !self.is_active() {
// Step 3.
return Ok(());
}
let parser = self.get_current_parser();
let parser = match parser.as_ref() {
Some(parser) if parser.script_nesting_level() > 0 => parser,
_ => {
// Either there is no parser, which means the parsing ended;
// or script nesting level is 0, which means the method was
// called from outside a parser-executed script.
if self.ignore_destructive_writes_counter.get() > 0 {
// Step 4.
// TODO: handle ignore-opens-during-unload counter.
return Ok(());
}
// Step 5.
// TODO: call document.open().
return Err(Error::InvalidState);
}
};
// Step 7.
// TODO: handle reload override buffer.
// Steps 6-8.
parser.write(text);
// Step 9.
Ok(())
}
// https://html.spec.whatwg.org/multipage/#dom-document-writeln
fn Writeln(&self, mut text: Vec<DOMString>) -> ErrorResult {
text.push("\n".into());
self.Write(text)
}
// https://html.spec.whatwg.org/multipage/#documentandelementeventhandlers
document_and_element_event_handlers!();
}

View file

@ -274,12 +274,10 @@ fn fetch_a_classic_script(script: &HTMLScriptElement,
impl HTMLScriptElement {
/// https://html.spec.whatwg.org/multipage/#prepare-a-script
///
/// Returns true if tokenization should continue, false otherwise.
pub fn prepare(&self) -> bool {
pub fn prepare(&self) {
// Step 1.
if self.already_started.get() {
return true;
return;
}
// Step 2.
@ -297,17 +295,17 @@ impl HTMLScriptElement {
// Step 4.
let text = self.Text();
if text.is_empty() && !element.has_attribute(&local_name!("src")) {
return true;
return;
}
// Step 5.
if !self.upcast::<Node>().is_in_doc() {
return true;
return;
}
// Step 6.
if !self.is_javascript() {
return true;
return;
}
// Step 7.
@ -322,12 +320,12 @@ impl HTMLScriptElement {
// Step 9.
let doc = document_from_node(self);
if self.parser_inserted.get() && &*self.parser_document != &*doc {
return true;
return;
}
// Step 10.
if !doc.is_scripting_enabled() {
return true;
return;
}
// TODO(#4577): Step 11: CSP.
@ -340,13 +338,13 @@ impl HTMLScriptElement {
let for_value = for_attribute.value().to_ascii_lowercase();
let for_value = for_value.trim_matches(HTML_SPACE_CHARACTERS);
if for_value != "window" {
return true;
return;
}
let event_value = event_attribute.value().to_ascii_lowercase();
let event_value = event_value.trim_matches(HTML_SPACE_CHARACTERS);
if event_value != "onload" && event_value != "onload()" {
return true;
return;
}
},
(_, _) => (),
@ -381,7 +379,7 @@ impl HTMLScriptElement {
// Step 18.2.
if src.is_empty() {
self.queue_error_event();
return true;
return;
}
// Step 18.4-18.5.
@ -389,7 +387,7 @@ impl HTMLScriptElement {
Err(_) => {
warn!("error parsing URL for script {}", &**src);
self.queue_error_event();
return true;
return;
}
Ok(url) => url,
};
@ -412,7 +410,6 @@ impl HTMLScriptElement {
!async {
doc.add_deferred_script(self);
// Second part implemented in Document::process_deferred_scripts.
return true;
// Step 20.b: classic, has src, was parser-inserted, is not async.
} else if is_external &&
was_parser_inserted &&
@ -432,7 +429,7 @@ impl HTMLScriptElement {
// Step 20.e: doesn't have src, was parser-inserted, is blocked on stylesheet.
} else if !is_external &&
was_parser_inserted &&
// TODO: check for script nesting levels.
doc.get_current_parser().map_or(false, |parser| parser.script_nesting_level() <= 1) &&
doc.get_script_blocking_stylesheets_count() > 0 {
doc.set_pending_parsing_blocking_script(Some(self));
*self.load.borrow_mut() = Some(Ok(ScriptOrigin::internal(text, base_url)));
@ -443,16 +440,7 @@ impl HTMLScriptElement {
self.ready_to_be_parser_executed.set(true);
*self.load.borrow_mut() = Some(Ok(ScriptOrigin::internal(text, base_url)));
self.execute();
return true;
}
// TODO: make this suspension happen automatically.
if was_parser_inserted {
if let Some(parser) = doc.get_current_parser() {
parser.suspend();
}
}
false
}
pub fn is_ready_to_be_executed(&self) -> bool {
@ -481,19 +469,20 @@ impl HTMLScriptElement {
Ok(script) => script,
};
if script.external {
debug!("loading external script, url = {}", script.url);
}
// TODO(#12446): beforescriptexecute.
if self.dispatch_before_script_execute_event() == EventStatus::Canceled {
return;
}
// Step 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.
let neutralized_doc = if script.external {
debug!("loading external script, url = {}", script.url);
let doc = document_from_node(self);
doc.incr_ignore_destructive_writes_counter();
Some(doc)
} else {
None
};
// Step 4.
let document = document_from_node(self);
@ -512,8 +501,9 @@ impl HTMLScriptElement {
document.set_current_script(old_script.r());
// Step 7.
// TODO: Decrement the ignore-destructive-writes counter of neutralised
// doc, if it was incremented in the earlier step.
if let Some(doc) = neutralized_doc {
doc.decr_ignore_destructive_writes_counter();
}
// TODO(#12446): afterscriptexecute.
self.dispatch_after_script_execute_event();

View file

@ -33,12 +33,25 @@ use profile_traits::time::{TimerMetadataReflowType, ProfilerCategory, profile};
use script_thread::ScriptThread;
use servo_url::ServoUrl;
use std::cell::Cell;
use std::mem;
use util::resource_files::read_resource_file;
mod html;
mod xml;
#[dom_struct]
/// The parser maintains two input streams: one for input from script through
/// document.write(), and one for input from network.
///
/// There is no concrete representation of the insertion point, instead it
/// always points to just before the next character from the network input,
/// with all of the script input before itself.
///
/// ```text
/// ... script input ... | ... network input ...
/// ^
/// insertion point
/// ```
pub struct ServoParser {
reflector: Reflector,
/// The document associated with this parser.
@ -46,15 +59,20 @@ pub struct ServoParser {
/// The pipeline associated with this parse, unavailable if this parse
/// does not correspond to a page load.
pipeline: Option<PipelineId>,
/// Input chunks received but not yet passed to the parser.
/// Input received from network.
#[ignore_heap_size_of = "Defined in html5ever"]
pending_input: DOMRefCell<BufferQueue>,
network_input: DOMRefCell<BufferQueue>,
/// Input received from script. Used only to support document.write().
#[ignore_heap_size_of = "Defined in html5ever"]
script_input: DOMRefCell<BufferQueue>,
/// The tokenizer of this parser.
tokenizer: DOMRefCell<Tokenizer>,
/// Whether to expect any further input from the associated network request.
last_chunk_received: Cell<bool>,
/// Whether this parser should avoid passing any further data to the tokenizer.
suspended: Cell<bool>,
/// https://html.spec.whatwg.org/multipage/#script-nesting-level
script_nesting_level: Cell<usize>,
}
#[derive(PartialEq)]
@ -134,6 +152,84 @@ impl ServoParser {
parser.parse_chunk(String::from(input));
}
pub fn script_nesting_level(&self) -> usize {
self.script_nesting_level.get()
}
/// Corresponds to the latter part of the "Otherwise" branch of the 'An end
/// tag whose tag name is "script"' of
/// https://html.spec.whatwg.org/multipage/#parsing-main-incdata
///
/// This first moves everything from the script input to the beginning of
/// the network input, effectively resetting the insertion point to just
/// before the next character to be consumed.
///
///
/// ```text
/// | ... script input ... network input ...
/// ^
/// insertion point
/// ```
pub fn resume_with_pending_parsing_blocking_script(&self, script: &HTMLScriptElement) {
assert!(self.suspended.get());
self.suspended.set(false);
mem::swap(&mut *self.script_input.borrow_mut(), &mut *self.network_input.borrow_mut());
while let Some(chunk) = self.script_input.borrow_mut().pop_front() {
self.network_input.borrow_mut().push_back(chunk);
}
let script_nesting_level = self.script_nesting_level.get();
assert_eq!(script_nesting_level, 0);
self.script_nesting_level.set(script_nesting_level + 1);
script.execute();
self.script_nesting_level.set(script_nesting_level);
if !self.suspended.get() {
self.parse_sync();
}
}
/// Steps 6-8 of https://html.spec.whatwg.org/multipage/#document.write()
pub fn write(&self, text: Vec<DOMString>) {
assert!(self.script_nesting_level.get() > 0);
if self.document.get_pending_parsing_blocking_script().is_some() {
// There is already a pending parsing blocking script so the
// parser is suspended, we just append everything to the
// script input and abort these steps.
for chunk in text {
self.script_input.borrow_mut().push_back(String::from(chunk).into());
}
return;
}
// There is no pending parsing blocking script, so all previous calls
// to document.write() should have seen their entire input tokenized
// and process, with nothing pushed to the parser script input.
assert!(self.script_input.borrow().is_empty());
let mut input = BufferQueue::new();
for chunk in text {
input.push_back(String::from(chunk).into());
}
self.tokenize(|tokenizer| tokenizer.feed(&mut input));
if self.suspended.get() {
// Parser got suspended, insert remaining input at end of
// script input, following anything written by scripts executed
// reentrantly during this call.
while let Some(chunk) = input.pop_front() {
self.script_input.borrow_mut().push_back(chunk);
}
return;
}
assert!(input.is_empty());
}
#[allow(unrooted_must_root)]
fn new_inherited(
document: &Document,
@ -145,10 +241,12 @@ impl ServoParser {
reflector: Reflector::new(),
document: JS::from_ref(document),
pipeline: pipeline,
pending_input: DOMRefCell::new(BufferQueue::new()),
network_input: DOMRefCell::new(BufferQueue::new()),
script_input: DOMRefCell::new(BufferQueue::new()),
tokenizer: DOMRefCell::new(tokenizer),
last_chunk_received: Cell::new(last_chunk_state == LastChunkState::Received),
suspended: Default::default(),
script_nesting_level: Default::default(),
}
}
@ -165,106 +263,87 @@ impl ServoParser {
ServoParserBinding::Wrap)
}
pub fn document(&self) -> &Document {
&self.document
}
pub fn pipeline(&self) -> Option<PipelineId> {
self.pipeline
}
fn has_pending_input(&self) -> bool {
!self.pending_input.borrow().is_empty()
}
fn push_input_chunk(&self, chunk: String) {
self.pending_input.borrow_mut().push_back(chunk.into());
}
fn last_chunk_received(&self) -> bool {
self.last_chunk_received.get()
}
fn mark_last_chunk_received(&self) {
self.last_chunk_received.set(true)
}
fn set_plaintext_state(&self) {
self.tokenizer.borrow_mut().set_plaintext_state()
}
pub fn suspend(&self) {
assert!(!self.suspended.get());
self.suspended.set(true);
}
pub fn resume(&self) {
assert!(self.suspended.get());
self.suspended.set(false);
self.parse_sync();
}
pub fn is_suspended(&self) -> bool {
self.suspended.get()
self.network_input.borrow_mut().push_back(chunk.into());
}
fn parse_sync(&self) {
let metadata = TimerMetadata {
url: self.document().url().as_str().into(),
url: self.document.url().as_str().into(),
iframe: TimerMetadataFrameType::RootWindow,
incremental: TimerMetadataReflowType::FirstReflow,
};
let profiler_category = self.tokenizer.borrow().profiler_category();
profile(profiler_category,
Some(metadata),
self.document().window().upcast::<GlobalScope>().time_profiler_chan().clone(),
self.document.window().upcast::<GlobalScope>().time_profiler_chan().clone(),
|| self.do_parse_sync())
}
fn do_parse_sync(&self) {
assert!(self.script_input.borrow().is_empty());
// This parser will continue to parse while there is either pending input or
// the parser remains unsuspended.
loop {
self.document().reflow_if_reflow_timer_expired();
if let Err(script) = self.tokenizer.borrow_mut().feed(&mut *self.pending_input.borrow_mut()) {
if script.prepare() {
continue;
}
}
// Document parsing is blocked on an external resource.
if self.suspended.get() {
return;
}
self.tokenize(|tokenizer| tokenizer.feed(&mut *self.network_input.borrow_mut()));
if !self.has_pending_input() {
break;
}
if self.suspended.get() {
return;
}
if self.last_chunk_received() {
assert!(self.network_input.borrow().is_empty());
if self.last_chunk_received.get() {
self.finish();
}
}
fn parse_chunk(&self, input: String) {
self.document().set_current_parser(Some(self));
self.document.set_current_parser(Some(self));
self.push_input_chunk(input);
if !self.is_suspended() {
if !self.suspended.get() {
self.parse_sync();
}
}
fn tokenize<F>(&self, mut feed: F)
where F: FnMut(&mut Tokenizer) -> Result<(), Root<HTMLScriptElement>>
{
loop {
assert!(!self.suspended.get());
self.document.reflow_if_reflow_timer_expired();
let script = match feed(&mut *self.tokenizer.borrow_mut()) {
Ok(()) => return,
Err(script) => script,
};
let script_nesting_level = self.script_nesting_level.get();
self.script_nesting_level.set(script_nesting_level + 1);
script.prepare();
self.script_nesting_level.set(script_nesting_level);
if self.document.get_pending_parsing_blocking_script().is_some() {
self.suspended.set(true);
return;
}
}
}
fn finish(&self) {
assert!(!self.suspended.get());
assert!(!self.has_pending_input());
assert!(self.last_chunk_received.get());
assert!(self.script_input.borrow().is_empty());
assert!(self.network_input.borrow().is_empty());
self.tokenizer.borrow_mut().end();
debug!("finished parsing");
self.document().set_current_parser(None);
self.document.set_current_parser(None);
if let Some(pipeline) = self.pipeline() {
if let Some(pipeline) = self.pipeline {
ScriptThread::parsing_complete(pipeline);
}
}
@ -372,7 +451,7 @@ impl FetchResponseListener for ParserContext {
parser.push_input_chunk(page);
parser.parse_sync();
let doc = parser.document();
let doc = &parser.document;
let doc_body = Root::upcast::<Node>(doc.GetBody().unwrap());
let img = HTMLImageElement::new(local_name!("img"), None, doc);
img.SetSrc(DOMString::from(self.url.to_string()));
@ -384,7 +463,7 @@ impl FetchResponseListener for ParserContext {
let page = "<pre>\n".into();
parser.push_input_chunk(page);
parser.parse_sync();
parser.set_plaintext_state();
parser.tokenizer.borrow_mut().set_plaintext_state();
},
Some(ContentType(Mime(TopLevel::Text, SubLevel::Html, _))) => { // Handle text/html
if let Some(reason) = ssl_error {
@ -449,11 +528,11 @@ impl FetchResponseListener for ParserContext {
debug!("Failed to load page URL {}, error: {:?}", self.url, err);
}
parser.document()
parser.document
.finish_load(LoadType::PageSource(self.url.clone()));
parser.mark_last_chunk_received();
if !parser.is_suspended() {
parser.last_chunk_received.set(true);
if !parser.suspended.get() {
parser.parse_sync();
}
}

View file

@ -114,8 +114,10 @@ partial /*sealed*/ interface Document {
// Document open(optional DOMString type = "text/html", optional DOMString replace = "");
// WindowProxy open(DOMString url, DOMString name, DOMString features, optional boolean replace = false);
// void close();
// void write(DOMString... text);
// void writeln(DOMString... text);
[Throws]
void write(DOMString... text);
[Throws]
void writeln(DOMString... text);
// user interaction
readonly attribute Window?/*Proxy?*/ defaultView;