diff --git a/components/layout/query.rs b/components/layout/query.rs index be3f68c72fe..b86c156d1db 100644 --- a/components/layout/query.rs +++ b/components/layout/query.rs @@ -972,8 +972,8 @@ enum InnerTextItem { RequiredLineBreakCount(u32), } -// https://html.spec.whatwg.org/multipage/#the-innertext-idl-attribute -pub fn process_element_inner_text_query<'dom>( +/// +pub fn get_the_text_steps<'dom>( node: impl LayoutNode<'dom>, indexable_text: &IndexableText, ) -> String { diff --git a/components/layout_2020/query.rs b/components/layout_2020/query.rs index 587a1bc8cdb..dddba280e45 100644 --- a/components/layout_2020/query.rs +++ b/components/layout_2020/query.rs @@ -12,10 +12,13 @@ use log::warn; use script_layout_interface::wrapper_traits::{ LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode, }; -use script_layout_interface::OffsetParentResponse; +use script_layout_interface::{LayoutElementType, LayoutNodeType, OffsetParentResponse}; use servo_arc::Arc as ServoArc; use servo_url::ServoUrl; +use style::computed_values::display::T as Display; use style::computed_values::position::T as Position; +use style::computed_values::visibility::T as Visibility; +use style::computed_values::white_space_collapse::T as WhiteSpaceCollapseValue; use style::context::{QuirksMode, SharedStyleContext, StyleContext, ThreadLocalStyleContext}; use style::dom::{OpaqueNode, TElement}; use style::properties::style_structs::Font; @@ -28,9 +31,11 @@ use style::shared_lock::SharedRwLock; use style::stylesheets::{CssRuleType, Origin, UrlExtraData}; use style::stylist::RuleInclusion; use style::traversal::resolve_style; +use style::values::computed::Float; use style::values::generics::font::LineHeight; use style_traits::{ParsingMode, ToCss}; +use crate::flow::inline::construct::{TextTransformation, WhitespaceCollapse}; use crate::fragment_tree::{BoxFragment, Fragment, FragmentFlags, FragmentTree, Tag}; pub fn process_content_box_request( @@ -507,9 +512,430 @@ fn is_eligible_parent(fragment: &BoxFragment) -> bool { .contains(FragmentFlags::IS_TABLE_TH_OR_TD_ELEMENT) } -// https://html.spec.whatwg.org/multipage/#the-innertext-idl-attribute -pub fn process_element_inner_text_query<'dom>(_node: impl LayoutNode<'dom>) -> String { - "".to_owned() +/// +pub fn get_the_text_steps<'dom>(node: impl LayoutNode<'dom>) -> String { + // Step 1: If element is not being rendered or if the user agent is a non-CSS user agent, then + // return element's descendant text content. + // This is taken care of in HTMLElemnent code + + // Step 2: Let results be a new empty list. + let mut results = Vec::new(); + let mut max_req_line_break_count = 0; + + // Step 3: For each child node node of element: + let mut state = Default::default(); + for child in node.dom_children() { + // Step 1: Let current be the list resulting in running the rendered text collection steps with node. + let mut current = rendered_text_collection_steps(child, &mut state); + // Step 2: For each item item in current, append item to results. + results.append(&mut current); + } + + let mut output = Vec::new(); + for item in results { + match item { + InnerOrOuterTextItem::Text(s) => { + // Step 3. + if !s.is_empty() { + if max_req_line_break_count > 0 { + // Step 5. + output.push("\u{000A}".repeat(max_req_line_break_count)); + max_req_line_break_count = 0; + } + output.push(s); + } + }, + InnerOrOuterTextItem::RequiredLineBreakCount(count) => { + // Step 4. + if output.is_empty() { + // Remove required line break count at the start. + continue; + } + // Store the count if it's the max of this run, but it may be ignored if no text + // item is found afterwards, which means that these are consecutive line breaks at + // the end. + if count > max_req_line_break_count { + max_req_line_break_count = count; + } + }, + } + } + output.into_iter().collect() +} + +enum InnerOrOuterTextItem { + Text(String), + RequiredLineBreakCount(usize), +} + +#[derive(Clone)] +struct RenderedTextCollectionState { + /// Used to make sure we don't add a `\n` before the first row + first_table_row: bool, + /// Used to make sure we don't add a `\t` before the first column + first_table_cell: bool, + /// Keeps track of whether we're inside a table, since there are special rules like ommiting everything that's not + /// inside a TableCell/TableCaption + within_table: bool, + /// Determines whether we truncate leading whitespaces for normal nodes or not + may_start_with_whitespace: bool, + /// Is set whenever we truncated a white space char, used to prepend a single space before the next element, + /// that way we truncate trailing white space without having to look ahead + did_truncate_trailing_white_space: bool, + /// Is set to true when we're rendering the children of TableCell/TableCaption elements, that way we render + /// everything inside those as normal, while omitting everything that's in a Table but NOT in a Cell/Caption + within_table_content: bool, +} + +impl Default for RenderedTextCollectionState { + fn default() -> Self { + RenderedTextCollectionState { + first_table_row: true, + first_table_cell: true, + may_start_with_whitespace: true, + did_truncate_trailing_white_space: false, + within_table: false, + within_table_content: false, + } + } +} + +/// +fn rendered_text_collection_steps<'dom>( + node: impl LayoutNode<'dom>, + state: &mut RenderedTextCollectionState, +) -> Vec { + // Step 1. Let items be the result of running the rendered text collection + // steps with each child node of node in tree order, + // and then concatenating the results to a single list. + let mut items = vec![]; + if !node.is_connected() || !(node.is_element() || node.is_text_node()) { + return items; + } + + match node.type_id() { + LayoutNodeType::Text => { + if let Some(element) = node.parent_node() { + match element.type_id() { + // Any text contained in these elements must be ignored. + LayoutNodeType::Element(LayoutElementType::HTMLCanvasElement) | + LayoutNodeType::Element(LayoutElementType::HTMLImageElement) | + LayoutNodeType::Element(LayoutElementType::HTMLIFrameElement) | + LayoutNodeType::Element(LayoutElementType::HTMLObjectElement) | + LayoutNodeType::Element(LayoutElementType::HTMLInputElement) | + LayoutNodeType::Element(LayoutElementType::HTMLTextAreaElement) | + LayoutNodeType::Element(LayoutElementType::HTMLMediaElement) => { + return items; + }, + // Select/Option/OptGroup elements are handled a bit differently. + // Basically: a Select can only contain Options or OptGroups, while + // OptGroups may also contain Options. Everything else gets ignored. + LayoutNodeType::Element(LayoutElementType::HTMLOptGroupElement) => { + if let Some(element) = element.parent_node() { + if !matches!( + element.type_id(), + LayoutNodeType::Element(LayoutElementType::HTMLSelectElement) + ) { + return items; + } + } else { + return items; + } + }, + LayoutNodeType::Element(LayoutElementType::HTMLSelectElement) => return items, + _ => {}, + } + + // Tables are also a bit special, mainly by only allowing + // content within TableCell or TableCaption elements once + // we're inside a Table. + if state.within_table && !state.within_table_content { + return items; + } + + let Some(style_data) = element.style_data() else { + return items; + }; + + let element_data = style_data.element_data.borrow(); + let Some(style) = element_data.styles.get_primary() else { + return items; + }; + + // Step 2: If node's computed value of 'visibility' is not 'visible', then return items. + // + // We need to do this check here on the Text fragment, if we did it on the element and + // just skipped rendering all child nodes then there'd be no way to override the + // visibility in a child node. + if style.get_inherited_box().visibility != Visibility::Visible { + return items; + } + + // Step 3: If node is not being rendered, then return items. For the purpose of this step, + // the following elements must act as described if the computed value of the 'display' + // property is not 'none': + let display = style.get_box().display; + if display == Display::None { + match element.type_id() { + // Even if set to Display::None, Option/OptGroup elements need to + // be rendered. + LayoutNodeType::Element(LayoutElementType::HTMLOptGroupElement) | + LayoutNodeType::Element(LayoutElementType::HTMLOptionElement) => {}, + _ => { + return items; + }, + } + } + + let text_content = node.to_threadsafe().node_text_content(); + + let white_space_collapse = style.clone_white_space_collapse(); + let preserve_whitespace = white_space_collapse == WhiteSpaceCollapseValue::Preserve; + let is_inline = matches!( + display, + Display::InlineBlock | Display::InlineFlex | Display::InlineGrid + ); + // Now we need to decide on whether to remove beginning white space or not, this + // is mainly decided by the elements we rendered before, but may be overwritten by the white-space + // property. + let trim_beginning_white_space = + !preserve_whitespace && (state.may_start_with_whitespace || is_inline); + let with_white_space_rules_applied = WhitespaceCollapse::new( + text_content.chars(), + white_space_collapse, + trim_beginning_white_space, + ); + + // Step 4: If node is a Text node, then for each CSS text box produced by node, in + // content order, compute the text of the box after application of the CSS + // 'white-space' processing rules and 'text-transform' rules, set items to the list + // of the resulting strings, and return items. The CSS 'white-space' processing + // rules are slightly modified: collapsible spaces at the end of lines are always + // collapsed, but they are only removed if the line is the last line of the block, + // or it ends with a br element. Soft hyphens should be preserved. + let mut transformed_text: String = TextTransformation::new( + with_white_space_rules_applied, + style.clone_text_transform().case(), + ) + .collect(); + + let is_preformatted_element = + white_space_collapse == WhiteSpaceCollapseValue::Preserve; + + let is_final_character_whitespace = transformed_text + .chars() + .next_back() + .filter(char::is_ascii_whitespace) + .is_some(); + + let is_first_character_whitespace = transformed_text + .chars() + .next() + .filter(char::is_ascii_whitespace) + .is_some(); + + // By truncating trailing white space and then adding it back in once we + // encounter another text node we can ensure no trailing white space for + // normal text without having to look ahead + if state.did_truncate_trailing_white_space && !is_first_character_whitespace { + items.push(InnerOrOuterTextItem::Text(String::from(" "))); + }; + + if transformed_text.len() > 0 { + // Here we decide whether to keep or truncate the final white + // space character, if there is one. + if is_final_character_whitespace && !is_preformatted_element { + state.may_start_with_whitespace = false; + state.did_truncate_trailing_white_space = true; + transformed_text.pop(); + } else { + state.may_start_with_whitespace = is_final_character_whitespace; + state.did_truncate_trailing_white_space = false; + } + items.push(InnerOrOuterTextItem::Text(transformed_text)); + } + } else { + // If we don't have a parent element then there's no style data available, + // in this (pretty unlikely) case we just return the Text fragment as is. + items.push(InnerOrOuterTextItem::Text( + node.to_threadsafe().node_text_content().into(), + )); + } + }, + LayoutNodeType::Element(LayoutElementType::HTMLBRElement) => { + // Step 5: If node is a br element, then append a string containing a single U+000A + // LF code point to items. + state.did_truncate_trailing_white_space = false; + state.may_start_with_whitespace = true; + items.push(InnerOrOuterTextItem::Text(String::from("\u{000A}"))); + }, + _ => { + // First we need to gather some infos to setup the various flags + // before rendering the child nodes + let Some(style_data) = node.style_data() else { + return items; + }; + + let element_data = style_data.element_data.borrow(); + let Some(style) = element_data.styles.get_primary() else { + return items; + }; + let inherited_box = style.get_inherited_box(); + + if inherited_box.visibility != Visibility::Visible { + // If the element is not visible then we'll immediatly render all children, + // skipping all other processing. + // We can't just stop here since a child can override a parents visibility. + for child in node.dom_children() { + items.append(&mut rendered_text_collection_steps(child, state)); + } + return items; + } + + let style_box = style.get_box(); + let display = style_box.display; + let mut surrounding_line_breaks = 0; + + // Treat absolutely positioned or floated elements like Block elements + if style_box.position == Position::Absolute || style_box.float != Float::None { + surrounding_line_breaks = 1; + } + + // Depending on the display property we have to do various things + // before we can render the child nodes. + match display { + Display::Table => { + surrounding_line_breaks = 1; + state.within_table = true; + }, + // Step 6: If node's computed value of 'display' is 'table-cell', + // and node's CSS box is not the last 'table-cell' box of its + // enclosing 'table-row' box, then append a string containing + // a single U+0009 TAB code point to items. + Display::TableCell => { + if !state.first_table_cell { + items.push(InnerOrOuterTextItem::Text(String::from( + "\u{0009}", /* tab */ + ))); + // Make sure we don't add a white-space we removed from the previous node + state.did_truncate_trailing_white_space = false; + } + state.first_table_cell = false; + state.within_table_content = true; + }, + // Step 7: If node's computed value of 'display' is 'table-row', + // and node's CSS box is not the last 'table-row' box of the nearest + // ancestor 'table' box, then append a string containing a single U+000A + // LF code point to items. + Display::TableRow => { + if !state.first_table_row { + items.push(InnerOrOuterTextItem::Text(String::from( + "\u{000A}", /* Line Feed */ + ))); + // Make sure we don't add a white-space we removed from the previous node + state.did_truncate_trailing_white_space = false; + } + state.first_table_row = false; + state.first_table_cell = true; + }, + // Step 9: If node's used value of 'display' is block-level or 'table-caption', + // then append 1 (a required line break count) at the beginning and end of items. + Display::Block => { + surrounding_line_breaks = 1; + }, + Display::TableCaption => { + surrounding_line_breaks = 1; + state.within_table_content = true; + }, + Display::InlineFlex | Display::InlineGrid | Display::InlineBlock => { + // InlineBlock's are a bit strange, in that they don't produce a Linebreak, yet + // disable white space truncation before and after it, making it one of the few + // cases where one can have multiple white space characters following one another. + if state.did_truncate_trailing_white_space { + items.push(InnerOrOuterTextItem::Text(String::from(" "))); + state.did_truncate_trailing_white_space = false; + state.may_start_with_whitespace = true; + } + }, + _ => {}, + } + + match node.type_id() { + // Step 8: If node is a p element, then append 2 (a required line break count) at + // the beginning and end of items. + LayoutNodeType::Element(LayoutElementType::HTMLParagraphElement) => { + surrounding_line_breaks = 2; + }, + // Option/OptGroup elements should go on separate lines, by treating them like + // Block elements we can achieve that. + LayoutNodeType::Element(LayoutElementType::HTMLOptionElement) | + LayoutNodeType::Element(LayoutElementType::HTMLOptGroupElement) => { + surrounding_line_breaks = 1; + }, + _ => {}, + } + + if surrounding_line_breaks > 0 { + items.push(InnerOrOuterTextItem::RequiredLineBreakCount( + surrounding_line_breaks, + )); + state.did_truncate_trailing_white_space = false; + state.may_start_with_whitespace = true; + } + + match node.type_id() { + // Any text/content contained in these elements is ignored. + // However we still need to check whether we have to prepend a + // space, since for example asd qwe must + // product "asd qwe" (note the 2 spaces) + LayoutNodeType::Element(LayoutElementType::HTMLCanvasElement) | + LayoutNodeType::Element(LayoutElementType::HTMLImageElement) | + LayoutNodeType::Element(LayoutElementType::HTMLIFrameElement) | + LayoutNodeType::Element(LayoutElementType::HTMLObjectElement) | + LayoutNodeType::Element(LayoutElementType::HTMLInputElement) | + LayoutNodeType::Element(LayoutElementType::HTMLTextAreaElement) | + LayoutNodeType::Element(LayoutElementType::HTMLMediaElement) => { + if display != Display::Block && state.did_truncate_trailing_white_space { + items.push(InnerOrOuterTextItem::Text(String::from(" "))); + state.did_truncate_trailing_white_space = false; + }; + state.may_start_with_whitespace = false; + }, + _ => { + // Now we can finally iterate over all children, appending whatever + // they produce to items. + for child in node.dom_children() { + items.append(&mut rendered_text_collection_steps(child, state)); + } + }, + } + + // Depending on the display property we still need to do some + // cleanup after rendering all child nodes + match display { + Display::InlineFlex | Display::InlineGrid | Display::InlineBlock => { + state.did_truncate_trailing_white_space = false; + state.may_start_with_whitespace = false; + }, + Display::Table => { + state.within_table = false; + }, + Display::TableCell | Display::TableCaption => { + state.within_table_content = false; + }, + _ => {}, + } + + if surrounding_line_breaks > 0 { + items.push(InnerOrOuterTextItem::RequiredLineBreakCount( + surrounding_line_breaks, + )); + state.did_truncate_trailing_white_space = false; + state.may_start_with_whitespace = true; + } + }, + }; + items } pub fn process_text_index_request(_node: OpaqueNode, _point: Point2D) -> Option { diff --git a/components/layout_thread/lib.rs b/components/layout_thread/lib.rs index fbf2621c8a9..765b5c3cd28 100644 --- a/components/layout_thread/lib.rs +++ b/components/layout_thread/lib.rs @@ -37,8 +37,8 @@ use layout::flow::{Flow, FlowFlags, GetBaseFlow, ImmutableFlowUtils, MutableOwne use layout::flow_ref::FlowRef; use layout::incremental::{RelayoutMode, SpecialRestyleDamage}; use layout::query::{ - process_client_rect_query, process_content_box_request, process_content_boxes_request, - process_element_inner_text_query, process_offset_parent_query, + get_the_text_steps, process_client_rect_query, process_content_box_request, + process_content_boxes_request, process_offset_parent_query, process_resolved_font_style_request, process_resolved_style_request, process_scrolling_area_request, }; @@ -325,12 +325,12 @@ impl Layout for LayoutThread { process_client_rect_query(node, root_flow_ref) } - fn query_element_inner_text( + fn query_element_inner_outer_text( &self, node: script_layout_interface::TrustedNodeAddress, ) -> String { let node = unsafe { ServoLayoutNode::new(&node) }; - process_element_inner_text_query(node, &self.indexable_text.borrow()) + get_the_text_steps(node, &self.indexable_text.borrow()) } fn query_inner_window_dimension( diff --git a/components/layout_thread_2020/lib.rs b/components/layout_thread_2020/lib.rs index fcb38b1caa2..c060f560ea7 100644 --- a/components/layout_thread_2020/lib.rs +++ b/components/layout_thread_2020/lib.rs @@ -30,7 +30,7 @@ use ipc_channel::ipc::IpcSender; use layout::context::LayoutContext; use layout::display_list::{DisplayList, WebRenderImageInfo}; use layout::query::{ - process_content_box_request, process_content_boxes_request, process_element_inner_text_query, + get_the_text_steps, process_content_box_request, process_content_boxes_request, process_node_geometry_request, process_node_scroll_area_request, process_offset_parent_query, process_resolved_font_style_query, process_resolved_style_request, process_text_index_request, }; @@ -299,12 +299,12 @@ impl Layout for LayoutThread { } #[tracing::instrument(skip(self), fields(servo_profiling = true))] - fn query_element_inner_text( + fn query_element_inner_outer_text( &self, node: script_layout_interface::TrustedNodeAddress, ) -> String { let node = unsafe { ServoLayoutNode::new(&node) }; - process_element_inner_text_query(node) + get_the_text_steps(node) } fn query_inner_window_dimension( diff --git a/components/script/dom/htmlelement.rs b/components/script/dom/htmlelement.rs index c0c2ac1dbd6..76bb021ea21 100644 --- a/components/script/dom/htmlelement.rs +++ b/components/script/dom/htmlelement.rs @@ -15,6 +15,7 @@ use style_dom::ElementState; use crate::dom::activation::Activatable; use crate::dom::attr::Attr; +use crate::dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterData_Binding::CharacterDataMethods; use crate::dom::bindings::codegen::Bindings::EventHandlerBinding::{ EventHandlerNonNull, OnErrorEventHandlerNonNull, }; @@ -26,6 +27,7 @@ use crate::dom::bindings::error::{Error, ErrorResult, Fallible}; use crate::dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementTypeId, NodeTypeId}; use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom}; use crate::dom::bindings::str::DOMString; +use crate::dom::characterdata::CharacterData; use crate::dom::cssstyledeclaration::{CSSModificationAccess, CSSStyleDeclaration, CSSStyleOwner}; use crate::dom::customelementregistry::CallbackReaction; use crate::dom::document::{Document, FocusType}; @@ -104,6 +106,30 @@ impl HTMLElement { let eventtarget = self.upcast::(); eventtarget.is::() || eventtarget.is::() } + + /// Calls into the layout engine to generate a plain text representation + /// of a [`HTMLElement`] as specified when getting the `.innerText` or + /// `.outerText` in JavaScript.` + /// + /// + fn get_inner_outer_text(&self) -> DOMString { + let node = self.upcast::(); + let window = window_from_node(node); + let element = self.as_element(); + + // Step 1. + let element_not_rendered = !node.is_connected() || !element.has_css_layout_box(); + if element_not_rendered { + return node.GetTextContent().unwrap(); + } + + window.layout_reflow(QueryMsg::ElementInnerOuterTextQuery); + let text = window + .layout() + .query_element_inner_outer_text(node.to_trusted_node_address()); + + DOMString::from(text) + } } impl HTMLElementMethods for HTMLElement { @@ -448,71 +474,69 @@ impl HTMLElementMethods for HTMLElement { rect.size.height.to_nearest_px() } - // https://html.spec.whatwg.org/multipage/#the-innertext-idl-attribute + /// fn InnerText(&self) -> DOMString { - let node = self.upcast::(); - let window = window_from_node(node); - let element = self.as_element(); - - // Step 1. - let element_not_rendered = !node.is_connected() || !element.has_css_layout_box(); - if element_not_rendered { - return node.GetTextContent().unwrap(); - } - - window.layout_reflow(QueryMsg::ElementInnerTextQuery); - let text = window - .layout() - .query_element_inner_text(node.to_trusted_node_address()); - DOMString::from(text) + self.get_inner_outer_text() } - // https://html.spec.whatwg.org/multipage/#the-innertext-idl-attribute + /// fn SetInnerText(&self, input: DOMString) { - // Step 1. + // Step 1: Let fragment be the rendered text fragment for value given element's node + // document. + let fragment = self.rendered_text_fragment(input); + + // Step 2: Replace all with fragment within element. + Node::replace_all(Some(fragment.upcast()), self.upcast::()); + } + + /// + fn GetOuterText(&self) -> Fallible { + Ok(self.get_inner_outer_text()) + } + + /// + fn SetOuterText(&self, input: DOMString) -> Fallible<()> { + // Step 1: If this's parent is null, then throw a "NoModificationAllowedError" DOMException. + let Some(parent) = self.upcast::().GetParentNode() else { + return Err(Error::NoModificationAllowed); + }; + + let node = self.upcast::(); let document = document_from_node(self); - // Step 2. - let fragment = DocumentFragment::new(&document); + // Step 2: Let next be this's next sibling. + let next = node.GetNextSibling(); - // Step 3. The given value is already named 'input'. + // Step 3: Let previous be this's previous sibling. + let previous = node.GetPreviousSibling(); - // Step 4. - let mut position = input.chars().peekable(); + // Step 4: Let fragment be the rendered text fragment for the given value given this's node + // document. + let fragment = self.rendered_text_fragment(input); - // Step 5. - let mut text = String::new(); + // Step 5: If fragment has no children, then append a new Text node whose data is the empty + // string and node document is this's node document to fragment. + if fragment.upcast::().children_count() == 0 { + let text_node = Text::new(DOMString::from("".to_owned()), &document); - // Step 6. - while let Some(ch) = position.next() { - match ch { - '\u{000A}' | '\u{000D}' => { - if ch == '\u{000D}' && position.peek() == Some(&'\u{000A}') { - // a \r\n pair should only generate one
, - // so just skip the \r. - position.next(); - } + fragment.upcast::().AppendChild(text_node.upcast())?; + } - if !text.is_empty() { - append_text_node_to_fragment(&document, &fragment, text); - text = String::new(); - } + // Step 6: Replace this with fragment within this's parent. + parent.ReplaceChild(fragment.upcast(), node)?; - let br = HTMLBRElement::new(local_name!("br"), None, &document, None); - fragment.upcast::().AppendChild(br.upcast()).unwrap(); - }, - _ => { - text.push(ch); - }, + // Step 7: If next is non-null and next's previous sibling is a Text node, then merge with + // the next text node given next's previous sibling. + if let Some(next_sibling) = next { + if let Some(node) = next_sibling.GetPreviousSibling() { + Self::merge_with_the_next_text_node(node); } } - if !text.is_empty() { - append_text_node_to_fragment(&document, &fragment, text); - } + // Step 8: If previous is a Text node, then merge with the next text node given previous. + previous.map(Self::merge_with_the_next_text_node); - // Step 7. - Node::replace_all(Some(fragment.upcast()), self.upcast::()); + Ok(()) } // https://html.spec.whatwg.org/multipage/#dom-translate @@ -897,6 +921,88 @@ impl HTMLElement { None => false, } } + + /// + fn rendered_text_fragment(&self, input: DOMString) -> DomRoot { + // Step 1: Let fragment be a new DocumentFragment whose node document is document. + let document = document_from_node(self); + let fragment = DocumentFragment::new(&document); + + // Step 2: Let position be a position variable for input, initially pointing at the start + // of input. + let mut position = input.chars().peekable(); + + // Step 3: Let text be the empty string. + let mut text = String::new(); + + // Step 4 + while let Some(ch) = position.next() { + match ch { + // While position is not past the end of input, and the code point at position is + // either U+000A LF or U+000D CR: + '\u{000A}' | '\u{000D}' => { + if ch == '\u{000D}' && position.peek() == Some(&'\u{000A}') { + // a \r\n pair should only generate one
, + // so just skip the \r. + position.next(); + } + + if !text.is_empty() { + append_text_node_to_fragment(&document, &fragment, text); + text = String::new(); + } + + let br = HTMLBRElement::new(local_name!("br"), None, &document, None); + fragment.upcast::().AppendChild(br.upcast()).unwrap(); + }, + _ => { + // Collect a sequence of code points that are not U+000A LF or U+000D CR from + // input given position, and set text to the result. + text.push(ch); + }, + } + } + + // If text is not the empty string, then append a new Text node whose data is text and node + // document is document to fragment. + if !text.is_empty() { + append_text_node_to_fragment(&document, &fragment, text); + } + + fragment + } + + /// Checks whether a given [`DomRoot`] and its next sibling are + /// of type [`Text`], and if so merges them into a single [`Text`] + /// node. + /// + /// + fn merge_with_the_next_text_node(node: DomRoot) { + // Make sure node is a Text node + if !node.is::() { + return; + } + + // Step 1: Let next be node's next sibling. + let next = match node.GetNextSibling() { + Some(next) => next, + None => return, + }; + + // Step 2: If next is not a Text node, then return. + if !next.is::() { + return; + } + // Step 3: Replace data with node, node's data's length, 0, and next's data. + let node_chars = node.downcast::().expect("Node is Text"); + let next_chars = next.downcast::().expect("Next node is Text"); + node_chars + .ReplaceData(node_chars.Length(), 0, next_chars.Data()) + .expect("Got chars from Text"); + + // Step 4:Remove next. + next.remove_self(); + } } impl VirtualMethods for HTMLElement { diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index 329089efda7..b5bdd05cbb7 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -3506,12 +3506,24 @@ impl From for LayoutElementType { ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLInputElement) => { LayoutElementType::HTMLInputElement }, + ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLOptGroupElement) => { + LayoutElementType::HTMLOptGroupElement + }, + ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLOptionElement) => { + LayoutElementType::HTMLOptionElement + }, ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLObjectElement) => { LayoutElementType::HTMLObjectElement }, ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLParagraphElement) => { LayoutElementType::HTMLParagraphElement }, + ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLPreElement) => { + LayoutElementType::HTMLPreElement + }, + ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLSelectElement) => { + LayoutElementType::HTMLSelectElement + }, ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLTableCellElement) => { LayoutElementType::HTMLTableCellElement }, diff --git a/components/script/dom/webidls/HTMLElement.webidl b/components/script/dom/webidls/HTMLElement.webidl index d094abd2a2a..a67a13d4be4 100644 --- a/components/script/dom/webidls/HTMLElement.webidl +++ b/components/script/dom/webidls/HTMLElement.webidl @@ -48,7 +48,8 @@ interface HTMLElement : Element { // attribute boolean spellcheck; // void forceSpellCheck(); - attribute [LegacyNullToEmptyString] DOMString innerText; + [CEReactions] attribute [LegacyNullToEmptyString] DOMString innerText; + [CEReactions, Throws] attribute [LegacyNullToEmptyString] DOMString outerText; [Throws] ElementInternals attachInternals(); diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index 80267002f15..1622eecc896 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -2737,7 +2737,7 @@ fn debug_reflow_events(id: PipelineId, reflow_goal: &ReflowGoal, reason: &Reflow QueryMsg::OffsetParentQuery => "\tOffsetParentQuery", QueryMsg::StyleQuery => "\tStyleQuery", QueryMsg::TextIndexQuery => "\tTextIndexQuery", - QueryMsg::ElementInnerTextQuery => "\tElementInnerTextQuery", + QueryMsg::ElementInnerOuterTextQuery => "\tElementInnerOuterTextQuery", QueryMsg::InnerWindowDimensionsQuery => "\tInnerWindowDimensionsQuery", }, }; diff --git a/components/shared/script_layout/lib.rs b/components/shared/script_layout/lib.rs index 67f95327453..1faff34b767 100644 --- a/components/shared/script_layout/lib.rs +++ b/components/shared/script_layout/lib.rs @@ -104,7 +104,11 @@ pub enum LayoutElementType { HTMLInputElement, HTMLMediaElement, HTMLObjectElement, + HTMLOptGroupElement, + HTMLOptionElement, HTMLParagraphElement, + HTMLPreElement, + HTMLSelectElement, HTMLTableCellElement, HTMLTableColElement, HTMLTableElement, @@ -239,7 +243,7 @@ pub trait Layout { fn query_content_box(&self, node: OpaqueNode) -> Option>; fn query_content_boxes(&self, node: OpaqueNode) -> Vec>; fn query_client_rect(&self, node: OpaqueNode) -> Rect; - fn query_element_inner_text(&self, node: TrustedNodeAddress) -> String; + fn query_element_inner_outer_text(&self, node: TrustedNodeAddress) -> String; fn query_inner_window_dimension( &self, context: BrowsingContextId, @@ -305,7 +309,7 @@ pub enum QueryMsg { NodesFromPointQuery, ResolvedStyleQuery, StyleQuery, - ElementInnerTextQuery, + ElementInnerOuterTextQuery, ResolvedFontStyleQuery, InnerWindowDimensionsQuery, } @@ -329,7 +333,7 @@ impl ReflowGoal { match *self { ReflowGoal::Full | ReflowGoal::TickAnimations | ReflowGoal::UpdateScrollNode(_) => true, ReflowGoal::LayoutQuery(ref querymsg) => match *querymsg { - QueryMsg::ElementInnerTextQuery | + QueryMsg::ElementInnerOuterTextQuery | QueryMsg::InnerWindowDimensionsQuery | QueryMsg::NodesFromPointQuery | QueryMsg::ResolvedStyleQuery | @@ -353,7 +357,7 @@ impl ReflowGoal { ReflowGoal::LayoutQuery(ref querymsg) => match *querymsg { QueryMsg::NodesFromPointQuery | QueryMsg::TextIndexQuery | - QueryMsg::ElementInnerTextQuery => true, + QueryMsg::ElementInnerOuterTextQuery => true, QueryMsg::ContentBox | QueryMsg::ContentBoxes | QueryMsg::ClientRectQuery | diff --git a/tests/wpt/meta-legacy-layout/custom-elements/reactions/HTMLElement.html.ini b/tests/wpt/meta-legacy-layout/custom-elements/reactions/HTMLElement.html.ini index 53c21f695d4..7999b67be3f 100644 --- a/tests/wpt/meta-legacy-layout/custom-elements/reactions/HTMLElement.html.ini +++ b/tests/wpt/meta-legacy-layout/custom-elements/reactions/HTMLElement.html.ini @@ -36,12 +36,6 @@ [spellcheck on HTMLElement must enqueue an attributeChanged reaction when replacing an existing attribute] expected: FAIL - [innerText on HTMLElement must enqueue a disconnected reaction] - expected: FAIL - - [outerText on HTMLElement must enqueue a disconnected reaction] - expected: FAIL - [popover on HTMLElement must enqueue an attributeChanged reaction when adding popover content attribute] expected: PRECONDITION_FAILED diff --git a/tests/wpt/meta-legacy-layout/html/dom/elements/the-innertext-and-outertext-properties/dynamic-getter.html.ini b/tests/wpt/meta-legacy-layout/html/dom/elements/the-innertext-and-outertext-properties/dynamic-getter.html.ini index d9f2ab4f99d..8e411fda557 100644 --- a/tests/wpt/meta-legacy-layout/html/dom/elements/the-innertext-and-outertext-properties/dynamic-getter.html.ini +++ b/tests/wpt/meta-legacy-layout/html/dom/elements/the-innertext-and-outertext-properties/dynamic-getter.html.ini @@ -1,21 +1,3 @@ [dynamic-getter.html] - [text-transform applied to child element ("
abc")] - expected: FAIL - - [text-transform applied to parent element ("
abc")] - expected: FAIL - - [display: none applied to child element ("
abc
def")] - expected: FAIL - - [display: none applied to parent element ("
invisible
abc")] - expected: FAIL - - [insert node into sub-tree ("
abc")] - expected: FAIL - - [remove node from sub-tree ("
abc
def")] - expected: FAIL - [insert whole sub-tree ("
")] expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/dom/elements/the-innertext-and-outertext-properties/getter-first-letter-marker-multicol.html.ini b/tests/wpt/meta-legacy-layout/html/dom/elements/the-innertext-and-outertext-properties/getter-first-letter-marker-multicol.html.ini deleted file mode 100644 index 72d703bb7f2..00000000000 --- a/tests/wpt/meta-legacy-layout/html/dom/elements/the-innertext-and-outertext-properties/getter-first-letter-marker-multicol.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[getter-first-letter-marker-multicol.html] - [Test innerText/outerText for a combination of a list item with ::first-letter in multicol] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/dom/elements/the-innertext-and-outertext-properties/getter.html.ini b/tests/wpt/meta-legacy-layout/html/dom/elements/the-innertext-and-outertext-properties/getter.html.ini index e73fa2e7229..7b02c88266b 100644 --- a/tests/wpt/meta-legacy-layout/html/dom/elements/the-innertext-and-outertext-properties/getter.html.ini +++ b/tests/wpt/meta-legacy-layout/html/dom/elements/the-innertext-and-outertext-properties/getter.html.ini @@ -1,106 +1,10 @@ [getter.html] - [Simplest possible test ("
abc")] - expected: FAIL - - [Leading whitespace removed ("
abc")] - expected: FAIL - - [Trailing whitespace removed ("
abc ")] - expected: FAIL - - [Internal whitespace compressed ("
abc def")] - expected: FAIL - - [\\n converted to space ("
abc\\ndef")] - expected: FAIL - - [\\r converted to space ("
abc\\rdef")] - expected: FAIL - - [\\t converted to space ("
abc\\tdef")] - expected: FAIL - [Trailing whitespace before hard line break removed ("
abc
def")] expected: FAIL [Leading whitespace after hard line break removed ("
abc
def")] expected: FAIL - [Leading whitespace preserved ("
 abc")]
-    expected: FAIL
-
-  [Trailing whitespace preserved ("
abc ")]
-    expected: FAIL
-
-  [Internal whitespace preserved ("
abc  def")]
-    expected: FAIL
-
-  [\\n preserved ("
abc\\ndef")]
-    expected: FAIL
-
-  [\\r converted to newline ("
abc\\rdef")]
-    expected: FAIL
-
-  [\\t preserved ("
abc\\tdef")]
-    expected: FAIL
-
-  [Two 
 siblings ("
abc
def
")] - expected: FAIL - - [Leading whitespace preserved ("
abc")] - expected: FAIL - - [Trailing whitespace preserved ("
abc ")] - expected: FAIL - - [Internal whitespace preserved ("
abc def")] - expected: FAIL - - [\\n preserved ("
abc\\ndef")] - expected: FAIL - - [\\r converted to newline ("
abc\\rdef")] - expected: FAIL - - [\\t preserved ("
abc\\tdef")] - expected: FAIL - - [Leading whitespace preserved (" abc")] - expected: FAIL - - [Trailing whitespace preserved ("abc ")] - expected: FAIL - - [Internal whitespace preserved ("abc def")] - expected: FAIL - - [\\n preserved ("abc\\ndef")] - expected: FAIL - - [\\r converted to newline ("abc\\rdef")] - expected: FAIL - - [\\t preserved ("abc\\tdef")] - expected: FAIL - - [Leading whitespace removed ("
abc")] - expected: FAIL - - [Trailing whitespace removed ("
abc ")] - expected: FAIL - - [Internal whitespace collapsed ("
abc def")] - expected: FAIL - - [\\n preserved ("
abc\\ndef")] - expected: FAIL - - [\\r converted to newline ("
abc\\rdef")] - expected: FAIL - - [\\t converted to space ("
abc\\tdef")] - expected: FAIL - [Whitespace collapses across element boundaries ("
abc def")] expected: FAIL @@ -119,39 +23,12 @@ [Trailing space at end of inline-block should be collapsed ("
abc def ghi")] expected: FAIL - [Whitespace between and block should be collapsed ("
abc
")] - expected: FAIL - - [Whitespace between inline-block and block should be collapsed ("
abc
")] - expected: FAIL - [Whitespace around should not be collapsed ("
abc def")] expected: FAIL [Whitespace around should not be collapsed ("
abc def")] expected: FAIL - [Leading whitesapce should not be collapsed ("
abc")] - expected: FAIL - - [Trailing whitesapce should not be collapsed ("
abc ")] - expected: FAIL - - [Whitespace around empty span should be collapsed ("
abc def")] - expected: FAIL - - [Whitespace around empty spans should be collapsed ("
abc def")] - expected: FAIL - - [ should not collapse following space ("
abc")] - expected: FAIL - - [Replaced element with display:block should be treated as block-level ("
abc def")] - expected: FAIL - - [Replaced element with display:block should be treated as block-level ("
abc def")] - expected: FAIL - [Soft line breaks ignored ("
abc def")] expected: FAIL @@ -212,48 +89,12 @@ [::first-letter float ignored ("
abc def")] expected: FAIL - [  preserved ("
 ")] - expected: FAIL - - [display:none container ("
abc")] - expected: FAIL - - [No whitespace compression in display:none container ("
abc def")] - expected: FAIL - - [No removal of leading/trailing whitespace in display:none container ("
abc def ")] - expected: FAIL - - [display:none child not rendered ("
123abc")] - expected: FAIL - - [display:none container with non-display-none target child ("
abc")] - expected: FAIL - - [non-display-none child of svg ("
abc")] - expected: FAIL - - [display:none child of svg ("