script/dom/execcommand/contenteditable/text.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use script_bindings::cell::Ref;
6use script_bindings::inheritance::Castable;
7use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
8
9use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
10use crate::dom::characterdata::CharacterData;
11use crate::dom::element::Element;
12use crate::dom::html::htmlbrelement::HTMLBRElement;
13use crate::dom::html::htmlimageelement::HTMLImageElement;
14use crate::dom::iterators::ShadowIncluding;
15use crate::dom::node::Node;
16use crate::dom::text::Text;
17
18impl Text {
19 /// <https://dom.spec.whatwg.org/#concept-cd-data>
20 pub(crate) fn data(&self) -> Ref<'_, String> {
21 self.upcast::<CharacterData>().data()
22 }
23
24 /// <https://w3c.github.io/editing/docs/execCommand/#whitespace-node>
25 pub(crate) fn is_whitespace_node(&self) -> bool {
26 // > A whitespace node is either a Text node whose data is the empty string;
27 let data = self.data();
28 if data.is_empty() {
29 return true;
30 }
31 // > or a Text node whose data consists only of one or more tabs (0x0009), line feeds (0x000A),
32 // > carriage returns (0x000D), and/or spaces (0x0020),
33 // > and whose parent is an Element whose resolved value for "white-space" is "normal" or "nowrap";
34 let Some(parent) = self.upcast::<Node>().GetParentElement() else {
35 return false;
36 };
37 // TODO: Optimize the below to only do a traversal once and in the match handle the expected collapse value
38 let Some(style) = parent.style() else {
39 return false;
40 };
41 let white_space_collapse = style.get_inherited_text().white_space_collapse;
42 if data
43 .bytes()
44 .all(|byte| matches!(byte, b'\t' | b'\n' | b'\r' | b' ')) &&
45 // Note that for "normal" and "nowrap", the longhand "white-space-collapse: collapse" applies
46 // https://www.w3.org/TR/css-text-4/#white-space-property
47 white_space_collapse == WhiteSpaceCollapse::Collapse
48 {
49 return true;
50 }
51 // > or a Text node whose data consists only of one or more tabs (0x0009), carriage returns (0x000D),
52 // > and/or spaces (0x0020), and whose parent is an Element whose resolved value for "white-space" is "pre-line".
53 data.bytes()
54 .all(|byte| matches!(byte, b'\t' | b'\r' | b' ')) &&
55 // Note that for "pre-line", the longhand "white-space-collapse: preserve-breaks" applies
56 // https://www.w3.org/TR/css-text-4/#white-space-property
57 white_space_collapse == WhiteSpaceCollapse::PreserveBreaks
58 }
59
60 /// <https://w3c.github.io/editing/docs/execCommand/#collapsed-whitespace-node>
61 pub(crate) fn is_collapsed_whitespace_node(&self) -> bool {
62 // Step 1. If node is not a whitespace node, return false.
63 if !self.is_whitespace_node() {
64 return false;
65 }
66 // Step 2. If node's data is the empty string, return true.
67 if self.data().is_empty() {
68 return true;
69 }
70 // Step 3. Let ancestor be node's parent.
71 let node = self.upcast::<Node>();
72 let Some(ancestor) = node.GetParentNode() else {
73 // Step 4. If ancestor is null, return true.
74 return true;
75 };
76 let mut resolved_ancestor = ancestor.clone();
77 for parent in ancestor.ancestors() {
78 // Step 5. If the "display" property of some ancestor of node has resolved value "none", return true.
79 if parent
80 .downcast::<Element>()
81 .is_some_and(Element::is_display_none)
82 {
83 return true;
84 }
85 // Step 6. While ancestor is not a block node and its parent is not null, set ancestor to its parent.
86 //
87 // Note that the spec is written as "while not". Since this is the end-condition, we need to invert
88 // the condition to decide when to stop.
89 if parent.is_block_node() {
90 break;
91 }
92 resolved_ancestor = parent;
93 }
94 // Step 7. Let reference be node.
95 // Step 8. While reference is a descendant of ancestor:
96 // Step 8.1. Let reference be the node before it in tree order.
97 for reference in node.preceding_nodes(&resolved_ancestor) {
98 // Step 8.2. If reference is a block node or a br, return true.
99 if reference.is_block_node() || reference.is::<HTMLBRElement>() {
100 return true;
101 }
102 // Step 8.3. If reference is a Text node that is not a whitespace node, or is an img, break from this loop.
103 if reference
104 .downcast::<Text>()
105 .is_some_and(|text| !text.is_whitespace_node()) ||
106 reference.is::<HTMLImageElement>()
107 {
108 break;
109 }
110 }
111 // Step 9. Let reference be node.
112 // Step 10. While reference is a descendant of ancestor:
113 // Step 10.1. Let reference be the node after it in tree order, or null if there is no such node.
114 for reference in node.following_nodes(&resolved_ancestor, ShadowIncluding::No) {
115 // Step 10.2. If reference is a block node or a br, return true.
116 if reference.is_block_node() || reference.is::<HTMLBRElement>() {
117 return true;
118 }
119 // Step 10.3. If reference is a Text node that is not a whitespace node, or is an img, break from this loop.
120 if reference
121 .downcast::<Text>()
122 .is_some_and(|text| !text.is_whitespace_node()) ||
123 reference.is::<HTMLImageElement>()
124 {
125 break;
126 }
127 }
128 // Step 11. Return false.
129 false
130 }
131
132 /// Part of <https://w3c.github.io/editing/docs/execCommand/#canonicalize-whitespace>
133 /// and deduplicated here, since we need to do this for both start and end nodes
134 pub(crate) fn has_whitespace_and_has_parent_with_whitespace_preserve(
135 &self,
136 offset: u32,
137 space_characters: &'static [&'static char],
138 ) -> bool {
139 // if node is a Text node and its parent's resolved value for "white-space" is neither "pre" nor "pre-wrap"
140 // and start offset is not zero and the (start offset − 1)st code unit of start node's data is a space (0x0020) or
141 // non-breaking space (0x00A0)
142 let has_preserve_space = self
143 .upcast::<Node>()
144 .GetParentNode()
145 .and_then(|parent_node| parent_node.downcast::<Element>().and_then(Element::style))
146 .is_some_and(|style| {
147 // Note that for "pre" and "pre-wrap", the longhand "white-space-collapse: preserve" applies
148 // https://www.w3.org/TR/css-text-4/#white-space-property
149 style.get_inherited_text().white_space_collapse != WhiteSpaceCollapse::Preserve
150 });
151 let has_space_character = self
152 .data()
153 .chars()
154 .nth(offset as usize)
155 .is_some_and(|c| space_characters.contains(&&c));
156 has_preserve_space && has_space_character
157 }
158}