mirror of
https://github.com/servo/servo.git
synced 2025-07-22 23:03:42 +01:00
Layout: Implement innerText/outerText (#33312)
* Implement outerText on HtmlElement Signed-off-by: Shane Handley <shanehandley@fastmail.com> * Fixed some innerText/outerText bugs Signed-off-by: Benjamin Vincent Schulenburg <bennyschulenburg@gmx.de> * Unified innerText/outerText handling outside of Layout Before these 2 were treated separately and only within Layout would they end up calling the same method, now they are already unified within HTMLElement Signed-off-by: Benjamin Vincent Schulenburg <bennyschulenburg@gmx.de> * Address a few nits Signed-off-by: Martin Robinson <mrobinson@igalia.com> * Added innerText support for `inline-flex` Signed-off-by: Benjamin Vincent Schulenburg <bennyschulenburg@gmx.de> --------- Signed-off-by: Shane Handley <shanehandley@fastmail.com> Signed-off-by: Benjamin Vincent Schulenburg <bennyschulenburg@gmx.de> Signed-off-by: Martin Robinson <mrobinson@igalia.com> Co-authored-by: Shane Handley <shanehandley@fastmail.com> Co-authored-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
parent
88ffe9f7a5
commit
dbd1666b17
26 changed files with 617 additions and 1625 deletions
|
@ -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>();
|
||||
eventtarget.is::<HTMLBodyElement>() || eventtarget.is::<HTMLFrameSetElement>()
|
||||
}
|
||||
|
||||
/// Calls into the layout engine to generate a plain text representation
|
||||
/// of a [`HTMLElement`] as specified when getting the `.innerText` or
|
||||
/// `.outerText` in JavaScript.`
|
||||
///
|
||||
/// <https://html.spec.whatwg.org/multipage/#get-the-text-steps>
|
||||
fn get_inner_outer_text(&self) -> DOMString {
|
||||
let node = self.upcast::<Node>();
|
||||
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
|
||||
/// <https://html.spec.whatwg.org/multipage/#the-innertext-idl-attribute>
|
||||
fn InnerText(&self) -> DOMString {
|
||||
let node = self.upcast::<Node>();
|
||||
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
|
||||
/// <https://html.spec.whatwg.org/multipage/#set-the-inner-text-steps>
|
||||
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::<Node>());
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#dom-outertext>
|
||||
fn GetOuterText(&self) -> Fallible<DOMString> {
|
||||
Ok(self.get_inner_outer_text())
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#the-innertext-idl-attribute:dom-outertext-2>
|
||||
fn SetOuterText(&self, input: DOMString) -> Fallible<()> {
|
||||
// Step 1: If this's parent is null, then throw a "NoModificationAllowedError" DOMException.
|
||||
let Some(parent) = self.upcast::<Node>().GetParentNode() else {
|
||||
return Err(Error::NoModificationAllowed);
|
||||
};
|
||||
|
||||
let node = self.upcast::<Node>();
|
||||
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::<Node>().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 <br>,
|
||||
// so just skip the \r.
|
||||
position.next();
|
||||
}
|
||||
fragment.upcast::<Node>().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::<Node>().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::<Node>());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-translate
|
||||
|
@ -897,6 +921,88 @@ impl HTMLElement {
|
|||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#rendered-text-fragment>
|
||||
fn rendered_text_fragment(&self, input: DOMString) -> DomRoot<DocumentFragment> {
|
||||
// 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 <br>,
|
||||
// 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::<Node>().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<Node>`] and its next sibling are
|
||||
/// of type [`Text`], and if so merges them into a single [`Text`]
|
||||
/// node.
|
||||
///
|
||||
/// <https://html.spec.whatwg.org/multipage/#merge-with-the-next-text-node>
|
||||
fn merge_with_the_next_text_node(node: DomRoot<Node>) {
|
||||
// Make sure node is a Text node
|
||||
if !node.is::<Text>() {
|
||||
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::<Text>() {
|
||||
return;
|
||||
}
|
||||
// Step 3: Replace data with node, node's data's length, 0, and next's data.
|
||||
let node_chars = node.downcast::<CharacterData>().expect("Node is Text");
|
||||
let next_chars = next.downcast::<CharacterData>().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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue