script/dom/execcommand/contenteditable/
element.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 html5ever::{LocalName, local_name};
6use js::context::JSContext;
7use script_bindings::inheritance::Castable;
8use style::attr::AttrValue;
9use style::properties::{LonghandId, PropertyDeclaration, PropertyDeclarationId, ShorthandId};
10use style::values::specified::TextDecorationLine;
11use style::values::specified::box_::DisplayOutside;
12
13use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
14use crate::dom::bindings::inheritance::{ElementTypeId, HTMLElementTypeId, NodeTypeId};
15use crate::dom::bindings::root::DomRoot;
16use crate::dom::bindings::str::DOMString;
17use crate::dom::element::Element;
18use crate::dom::execcommand::basecommand::{CommandName, CssPropertyName};
19use crate::dom::execcommand::commands::fontsize::font_size_to_css_font;
20use crate::dom::execcommand::contenteditable::node::move_preserving_ranges;
21use crate::dom::html::htmlfontelement::HTMLFontElement;
22use crate::dom::node::node::{Node, NodeTraits};
23
24impl Element {
25    pub(crate) fn resolved_display_value(&self) -> Option<DisplayOutside> {
26        self.style().map(|style| style.get_box().display.outside())
27    }
28
29    /// <https://w3c.github.io/editing/docs/execCommand/#specified-command-value>
30    pub(crate) fn specified_command_value(&self, command: &CommandName) -> Option<DOMString> {
31        match command {
32            // Step 1. If command is "backColor" or "hiliteColor" and the Element's display property does not have resolved value "inline", return null.
33            CommandName::BackColor | CommandName::HiliteColor => {
34                // TODO
35            },
36            // Step 2. If command is "createLink" or "unlink":
37            CommandName::CreateLink | CommandName::Unlink => {
38                // TODO
39            },
40            // Step 3. If command is "subscript" or "superscript":
41            CommandName::Subscript | CommandName::Superscript => {
42                // TODO
43            },
44            CommandName::Strikethrough => {
45                // Step 4. If command is "strikethrough", and element has a style attribute set, and that attribute sets "text-decoration":
46                if let Some(value) = CssPropertyName::TextDecorationLine.value_set_for_style(self) {
47                    // Step 4.1. If element's style attribute sets "text-decoration" to a value containing "line-through", return "line-through".
48                    // Step 4.2. Return null.
49                    return Some("line-through".into()).filter(|_| value.contains("line-through"));
50                }
51                // Step 5. If command is "strikethrough" and element is an s or strike element, return "line-through".
52                if matches!(*self.local_name(), local_name!("s") | local_name!("strike")) {
53                    return Some("line-through".into());
54                }
55            },
56            CommandName::Underline => {
57                // Step 6. If command is "underline", and element has a style attribute set, and that attribute sets "text-decoration":
58                if let Some(value) = CssPropertyName::TextDecorationLine.value_set_for_style(self) {
59                    // Step 6.1. If element's style attribute sets "text-decoration" to a value containing "underline", return "underline".
60                    // Step 6.2. Return null.
61                    return Some("underline".into()).filter(|_| value.contains("underline"));
62                }
63                // Step 7. If command is "underline" and element is a u element, return "underline".
64                if *self.local_name() == local_name!("u") {
65                    return Some("underline".into());
66                }
67            },
68            _ => {},
69        };
70        // Step 8. Let property be the relevant CSS property for command.
71        // Step 9. If property is null, return null.
72        let property = command.relevant_css_property()?;
73        // Step 10. If element has a style attribute set, and that attribute has the effect of setting property,
74        // return the value that it sets property to.
75        if let Some(value) = property.value_set_for_style(self) {
76            return Some(value);
77        }
78        // Step 11. If element is a font element that has an attribute whose effect is to create a presentational hint for property,
79        // return the value that the hint sets property to. (For a size of 7, this will be the non-CSS value "xxx-large".)
80        if self.is::<HTMLFontElement>() {
81            if let Some(font_size) = self.get_attribute(&local_name!("size")) {
82                if let AttrValue::UInt(_, value) = *font_size.value() {
83                    return Some(font_size_to_css_font(&value).into());
84                }
85            }
86        }
87
88        // Step 12. If element is in the following list, and property is equal to the CSS property name listed for it,
89        // return the string listed for it.
90        let element_name = self.local_name();
91        match property {
92            CssPropertyName::FontWeight
93                if element_name == &local_name!("b") || element_name == &local_name!("strong") =>
94            {
95                Some("bold".into())
96            },
97            CssPropertyName::FontStyle
98                if element_name == &local_name!("i") || element_name == &local_name!("em") =>
99            {
100                Some("italic".into())
101            },
102            // Step 13. Return null.
103            _ => None,
104        }
105    }
106
107    /// <https://w3c.github.io/editing/docs/execCommand/#modifiable-element>
108    pub(crate) fn is_modifiable_element(&self) -> bool {
109        let attrs = self.attrs();
110        let mut attrs = attrs.iter();
111        let type_id = self.upcast::<Node>().type_id();
112
113        // > A modifiable element is a b, em, i, s, span, strike, strong, sub, sup, or u element
114        // > with no attributes except possibly style;
115        if matches!(
116            type_id,
117            NodeTypeId::Element(ElementTypeId::HTMLElement(
118                HTMLElementTypeId::HTMLSpanElement,
119            ))
120        ) || matches!(
121            *self.local_name(),
122            local_name!("b") |
123                local_name!("em") |
124                local_name!("i") |
125                local_name!("s") |
126                local_name!("strike") |
127                local_name!("strong") |
128                local_name!("sub") |
129                local_name!("sup") |
130                local_name!("u")
131        ) {
132            return attrs.all(|attr| attr.local_name() == &local_name!("style"));
133        }
134
135        // > or a font element with no attributes except possibly style, color, face, and/or size;
136        if matches!(
137            type_id,
138            NodeTypeId::Element(ElementTypeId::HTMLElement(
139                HTMLElementTypeId::HTMLFontElement,
140            ))
141        ) {
142            return attrs.all(|attr| {
143                matches!(
144                    *attr.local_name(),
145                    local_name!("style") |
146                        local_name!("color") |
147                        local_name!("face") |
148                        local_name!("size")
149                )
150            });
151        }
152
153        // > or an a element with no attributes except possibly style and/or href.
154        if matches!(
155            type_id,
156            NodeTypeId::Element(ElementTypeId::HTMLElement(
157                HTMLElementTypeId::HTMLAnchorElement,
158            ))
159        ) {
160            return attrs.all(|attr| {
161                matches!(
162                    *attr.local_name(),
163                    local_name!("style") | local_name!("href")
164                )
165            });
166        }
167
168        false
169    }
170
171    pub(crate) fn has_empty_style_attribute(&self) -> bool {
172        let style_attribute = self.style_attribute().borrow();
173        style_attribute.as_ref().is_some_and(|declarations| {
174            let document = self.owner_document();
175            let shared_lock = document.style_shared_lock();
176            let read_lock = shared_lock.read();
177            let style = declarations.read_with(&read_lock);
178
179            style.is_empty()
180        })
181    }
182
183    /// <https://w3c.github.io/editing/docs/execCommand/#simple-modifiable-element>
184    pub(crate) fn is_simple_modifiable_element(&self) -> bool {
185        let attrs = self.attrs();
186        let attr_count = attrs.len();
187        let type_id = self.upcast::<Node>().type_id();
188
189        if matches!(
190            type_id,
191            NodeTypeId::Element(ElementTypeId::HTMLElement(
192                HTMLElementTypeId::HTMLAnchorElement,
193            )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
194                HTMLElementTypeId::HTMLFontElement,
195            )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
196                HTMLElementTypeId::HTMLSpanElement,
197            ))
198        ) || matches!(
199            *self.local_name(),
200            local_name!("b") |
201                local_name!("em") |
202                local_name!("i") |
203                local_name!("s") |
204                local_name!("strike") |
205                local_name!("strong") |
206                local_name!("sub") |
207                local_name!("sup") |
208                local_name!("u")
209        ) {
210            // > It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u element with no attributes.
211            if attr_count == 0 {
212                return true;
213            }
214
215            // > It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u element
216            // > with exactly one attribute, which is style,
217            // > which sets no CSS properties (including invalid or unrecognized properties).
218            if attr_count == 1 &&
219                attrs.first().expect("Size is 1").local_name() == &local_name!("style") &&
220                self.has_empty_style_attribute()
221            {
222                return true;
223            }
224        }
225
226        if attr_count != 1 {
227            return false;
228        }
229
230        let only_attribute = attrs.first().expect("Size is 1").local_name();
231
232        // > It is an a element with exactly one attribute, which is href.
233        if matches!(
234            type_id,
235            NodeTypeId::Element(ElementTypeId::HTMLElement(
236                HTMLElementTypeId::HTMLAnchorElement,
237            ))
238        ) {
239            return only_attribute == &local_name!("href");
240        }
241
242        // > It is a font element with exactly one attribute, which is either color, face, or size.
243        if matches!(
244            type_id,
245            NodeTypeId::Element(ElementTypeId::HTMLElement(
246                HTMLElementTypeId::HTMLFontElement,
247            ))
248        ) {
249            return only_attribute == &local_name!("color") ||
250                only_attribute == &local_name!("face") ||
251                only_attribute == &local_name!("size");
252        }
253
254        if only_attribute != &local_name!("style") {
255            return false;
256        }
257        let style_attribute = self.style_attribute().borrow();
258        let Some(declarations) = style_attribute.as_ref() else {
259            return false;
260        };
261        let document = self.owner_document();
262        let shared_lock = document.style_shared_lock();
263        let read_lock = shared_lock.read();
264        let style = declarations.read_with(&read_lock);
265
266        // > It is a b or strong element with exactly one attribute, which is style,
267        // > and the style attribute sets exactly one CSS property
268        // > (including invalid or unrecognized properties), which is "font-weight".
269        if matches!(*self.local_name(), local_name!("b") | local_name!("strong")) {
270            return style.len() == 1 &&
271                style.contains(PropertyDeclarationId::Longhand(LonghandId::FontWeight));
272        }
273
274        // > It is an i or em element with exactly one attribute, which is style,
275        // > and the style attribute sets exactly one CSS property (including invalid or unrecognized properties),
276        // > which is "font-style".
277        if matches!(*self.local_name(), local_name!("i") | local_name!("em")) {
278            return style.len() == 1 &&
279                style.contains(PropertyDeclarationId::Longhand(LonghandId::FontStyle));
280        }
281
282        let a_font_or_span = matches!(
283            type_id,
284            NodeTypeId::Element(ElementTypeId::HTMLElement(
285                HTMLElementTypeId::HTMLAnchorElement,
286            )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
287                HTMLElementTypeId::HTMLFontElement,
288            )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
289                HTMLElementTypeId::HTMLSpanElement,
290            ))
291        );
292        let s_strike_or_u = matches!(
293            *self.local_name(),
294            local_name!("s") | local_name!("strike") | local_name!("u")
295        );
296        if a_font_or_span || s_strike_or_u {
297            // Note that the shorthand "text-decoration" expands to 3 longhands. Hence we check if the length
298            // is 3 here, instead of 1.
299            if style.len() == 3 &&
300                style
301                    .shorthand_to_css(ShorthandId::TextDecoration, &mut String::new())
302                    .is_ok()
303            {
304                if let Some((text_decoration, _)) = style.get(PropertyDeclarationId::Longhand(
305                    LonghandId::TextDecorationLine,
306                )) {
307                    // > It is an a, font, s, span, strike, or u element with exactly one attribute,
308                    // > which is style, and the style attribute sets exactly one CSS property
309                    // > (including invalid or unrecognized properties), which is "text-decoration",
310                    // > which is set to "line-through" or "underline" or "overline" or "none".
311                    return matches!(
312                        text_decoration,
313                        PropertyDeclaration::TextDecorationLine(
314                            TextDecorationLine::LINE_THROUGH |
315                                TextDecorationLine::UNDERLINE |
316                                TextDecorationLine::OVERLINE |
317                                TextDecorationLine::NONE
318                        )
319                    );
320                }
321            } else if a_font_or_span {
322                // > It is an a, font, or span element with exactly one attribute, which is style,
323                // > and the style attribute sets exactly one CSS property (including invalid or unrecognized properties),
324                // > and that property is not "text-decoration".
325                return style.len() == 1;
326            }
327        }
328
329        false
330    }
331
332    /// <https://w3c.github.io/editing/docs/execCommand/#set-the-tag-name>
333    pub(crate) fn set_the_tag_name(&self, cx: &mut JSContext, new_name: &str) -> DomRoot<Element> {
334        // Step 1. If element is an HTML element with local name equal to new name, return element.
335        if self.local_name() == &LocalName::from(new_name) {
336            return DomRoot::from_ref(self);
337        }
338        // Step 2. If element's parent is null, return element.
339        let node = self.upcast::<Node>();
340        let Some(parent) = node.GetParentNode() else {
341            return DomRoot::from_ref(self);
342        };
343        // Step 3. Let replacement element be the result of calling createElement(new name) on the ownerDocument of element.
344        let document = node.owner_document();
345        let replacement = document.create_element(cx, new_name);
346        let replacement_node = replacement.upcast::<Node>();
347        // Step 4. Insert replacement element into element's parent immediately before element.
348        if parent
349            .InsertBefore(cx, replacement_node, Some(node))
350            .is_err()
351        {
352            unreachable!("Must always be able to insert");
353        }
354        // Step 5. Copy all attributes of element to replacement element, in order.
355        self.copy_all_attributes_to_other_element(cx, &replacement);
356        // Step 6. While element has children, append the first child of element as the last child of replacement element, preserving ranges.
357        for child in node.children() {
358            move_preserving_ranges(cx, &child, |cx| replacement_node.AppendChild(cx, &child));
359        }
360        // Step 7. Remove element from its parent.
361        node.remove_self(cx);
362        // Step 8. Return replacement element.
363        replacement
364    }
365}