Skip to main content

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