Skip to main content

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, ns};
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::htmlanchorelement::HTMLAnchorElement;
22use crate::dom::html::htmlfontelement::HTMLFontElement;
23use crate::dom::node::node::{Node, NodeTraits};
24
25impl Element {
26    pub(crate) fn resolved_display_value(&self) -> Option<DisplayOutside> {
27        self.style().map(|style| style.get_box().display.outside())
28    }
29
30    /// <https://w3c.github.io/editing/docs/execCommand/#specified-command-value>
31    pub(crate) fn specified_command_value(&self, command: &CommandName) -> Option<DOMString> {
32        match command {
33            // Step 1. If command is "backColor" or "hiliteColor" and the Element's display property does not have resolved value "inline", return null.
34            CommandName::BackColor | CommandName::HiliteColor
35                if self
36                    .resolved_display_value()
37                    .is_none_or(|display| display != DisplayOutside::Inline) =>
38            {
39                return None;
40            },
41            // Step 2. If command is "createLink" or "unlink":
42            CommandName::CreateLink | CommandName::Unlink => {
43                // Step 2.1. If element is an a element and has an href attribute,
44                // return the value of that attribute.
45                if let Some(anchor) = self.downcast::<HTMLAnchorElement>() {
46                    return anchor
47                        .upcast::<Element>()
48                        .get_attribute_string_value(&local_name!("href"))
49                        .map(|value| value.into());
50                }
51
52                // Step 2.2. Return null.
53                return None;
54            },
55            // Step 3. If command is "subscript" or "superscript":
56            CommandName::Subscript | CommandName::Superscript => {
57                // Step 3.1. If element is a sup, return "superscript".
58                if matches!(*self.local_name(), local_name!("sup")) {
59                    return Some("superscript".into());
60                }
61                // Step 3.2. If element is a sub, return "subscript".
62                if matches!(*self.local_name(), local_name!("sub")) {
63                    return Some("subscript".into());
64                }
65                // Step 3.3. Return null.
66                return None;
67            },
68            CommandName::Strikethrough => {
69                // Step 4. If command is "strikethrough", and element has a style attribute set, and that attribute sets "text-decoration":
70                if let Some(value) = CssPropertyName::TextDecorationLine.value_set_for_style(self) {
71                    // Step 4.1. If element's style attribute sets "text-decoration" to a value containing "line-through", return "line-through".
72                    // Step 4.2. Return null.
73                    return value
74                        .contains("line-through")
75                        .then_some("line-through".into());
76                }
77                // Step 5. If command is "strikethrough" and element is an s or strike element, return "line-through".
78                if matches!(*self.local_name(), local_name!("s") | local_name!("strike")) {
79                    return Some("line-through".into());
80                }
81            },
82            CommandName::Underline => {
83                // Step 6. If command is "underline", and element has a style attribute set, and that attribute sets "text-decoration":
84                if let Some(value) = CssPropertyName::TextDecorationLine.value_set_for_style(self) {
85                    // Step 6.1. If element's style attribute sets "text-decoration" to a value containing "underline", return "underline".
86                    // Step 6.2. Return null.
87                    return value.contains("underline").then_some("underline".into());
88                }
89                // Step 7. If command is "underline" and element is a u element, return "underline".
90                if *self.local_name() == local_name!("u") {
91                    return Some("underline".into());
92                }
93            },
94            _ => {},
95        };
96        // Step 8. Let property be the relevant CSS property for command.
97        // Step 9. If property is null, return null.
98        let property = command.relevant_css_property()?;
99        // Step 10. If element has a style attribute set, and that attribute has the effect of setting property,
100        // return the value that it sets property to.
101        if let Some(value) = property.value_set_for_style(self) {
102            return Some(value);
103        }
104        // Step 11. If element is a font element that has an attribute whose effect is to create a presentational hint for property,
105        // return the value that the hint sets property to. (For a size of 7, this will be the non-CSS value "xxx-large".)
106        if self.is::<HTMLFontElement>() &&
107            let Some(font_size) = self
108                .with_attribute(&ns!(), &local_name!("size"), |attribute| {
109                    if let AttrValue::UInt(_, value) = *attribute.value() {
110                        Some(value)
111                    } else {
112                        None
113                    }
114                })
115                .flatten()
116        {
117            return Some(font_size_to_css_font(&font_size).into());
118        }
119
120        // Step 12. If element is in the following list, and property is equal to the CSS property name listed for it,
121        // return the string listed for it.
122        let element_name = self.local_name();
123        match property {
124            CssPropertyName::FontWeight
125                if element_name == &local_name!("b") || element_name == &local_name!("strong") =>
126            {
127                Some("bold".into())
128            },
129            CssPropertyName::FontStyle
130                if element_name == &local_name!("i") || element_name == &local_name!("em") =>
131            {
132                Some("italic".into())
133            },
134            // Step 13. Return null.
135            _ => None,
136        }
137    }
138
139    /// <https://w3c.github.io/editing/docs/execCommand/#modifiable-element>
140    pub(crate) fn is_modifiable_element(&self) -> bool {
141        let attrs = self.attrs().borrow();
142        let mut attrs = attrs.iter();
143        let type_id = self.upcast::<Node>().type_id();
144
145        // > A modifiable element is a b, em, i, s, span, strike, strong, sub, sup, or u element
146        // > with no attributes except possibly style;
147        if matches!(
148            type_id,
149            NodeTypeId::Element(ElementTypeId::HTMLElement(
150                HTMLElementTypeId::HTMLSpanElement,
151            ))
152        ) || matches!(
153            *self.local_name(),
154            local_name!("b") |
155                local_name!("em") |
156                local_name!("i") |
157                local_name!("s") |
158                local_name!("strike") |
159                local_name!("strong") |
160                local_name!("sub") |
161                local_name!("sup") |
162                local_name!("u")
163        ) {
164            return attrs.all(|attr| attr.local_name() == &local_name!("style"));
165        }
166
167        // > or a font element with no attributes except possibly style, color, face, and/or size;
168        if matches!(
169            type_id,
170            NodeTypeId::Element(ElementTypeId::HTMLElement(
171                HTMLElementTypeId::HTMLFontElement,
172            ))
173        ) {
174            return attrs.all(|attr| {
175                matches!(
176                    *attr.local_name(),
177                    local_name!("style") |
178                        local_name!("color") |
179                        local_name!("face") |
180                        local_name!("size")
181                )
182            });
183        }
184
185        // > or an a element with no attributes except possibly style and/or href.
186        if matches!(
187            type_id,
188            NodeTypeId::Element(ElementTypeId::HTMLElement(
189                HTMLElementTypeId::HTMLAnchorElement,
190            ))
191        ) {
192            return attrs.all(|attr| {
193                matches!(
194                    *attr.local_name(),
195                    local_name!("style") | local_name!("href")
196                )
197            });
198        }
199
200        false
201    }
202
203    // <https://w3c.github.io/editing/docs/execCommand/#indentation-element>
204    pub(crate) fn is_indentation_element(&self) -> bool {
205        // > An indentation element is either a blockquote, or a div
206        if !matches!(
207            *self.local_name(),
208            local_name!("blockquote") | local_name!("div")
209        ) {
210            return false;
211        }
212
213        // > that has a style attribute that sets "margin" or some subproperty of it.
214        let style_attribute = self.style_attribute().borrow();
215        let Some(declarations) = style_attribute.as_ref() else {
216            return false;
217        };
218        let document = self.owner_document();
219        let shared_lock = document.style_shared_author_lock();
220        let read_lock = shared_lock.read();
221        let style = declarations.read_with(&read_lock);
222
223        ShorthandId::Margin
224            .longhands()
225            .any(|longhand| style.contains(PropertyDeclarationId::Longhand(longhand)))
226    }
227
228    pub(crate) fn has_empty_style_attribute(&self) -> bool {
229        let style_attribute = self.style_attribute().borrow();
230        style_attribute.as_ref().is_some_and(|declarations| {
231            let document = self.owner_document();
232            let shared_lock = document.style_shared_author_lock();
233            let read_lock = shared_lock.read();
234            let style = declarations.read_with(&read_lock);
235
236            style.is_empty()
237        })
238    }
239
240    /// <https://w3c.github.io/editing/docs/execCommand/#non-list-single-line-container>
241    pub(crate) fn is_non_list_single_line_container(&self) -> bool {
242        // > A non-list single-line container is an HTML element with local name
243        // > "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "listing", "p", "pre", or "xmp".
244        matches!(
245            *self.local_name(),
246            local_name!("address") |
247                local_name!("div") |
248                local_name!("h1") |
249                local_name!("h2") |
250                local_name!("h3") |
251                local_name!("h4") |
252                local_name!("h5") |
253                local_name!("h6") |
254                local_name!("listing") |
255                local_name!("p") |
256                local_name!("pre") |
257                local_name!("xmp")
258        )
259    }
260
261    /// <https://w3c.github.io/editing/docs/execCommand/#simple-modifiable-element>
262    pub(crate) fn is_simple_modifiable_element(&self) -> bool {
263        let attrs = self.attrs().borrow();
264        let attr_count = attrs.len();
265        let type_id = self.upcast::<Node>().type_id();
266
267        if matches!(
268            type_id,
269            NodeTypeId::Element(ElementTypeId::HTMLElement(
270                HTMLElementTypeId::HTMLAnchorElement,
271            )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
272                HTMLElementTypeId::HTMLFontElement,
273            )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
274                HTMLElementTypeId::HTMLSpanElement,
275            ))
276        ) || matches!(
277            *self.local_name(),
278            local_name!("b") |
279                local_name!("em") |
280                local_name!("i") |
281                local_name!("s") |
282                local_name!("strike") |
283                local_name!("strong") |
284                local_name!("sub") |
285                local_name!("sup") |
286                local_name!("u")
287        ) {
288            // > It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u element with no attributes.
289            if attr_count == 0 {
290                return true;
291            }
292
293            // > It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u element
294            // > with exactly one attribute, which is style,
295            // > which sets no CSS properties (including invalid or unrecognized properties).
296            if attr_count == 1 &&
297                attrs.first().expect("Size is 1").local_name() == &local_name!("style") &&
298                self.has_empty_style_attribute()
299            {
300                return true;
301            }
302        }
303
304        if attr_count != 1 {
305            return false;
306        }
307
308        let first_attr = attrs.first().expect("Size is 1");
309        let only_attribute = first_attr.local_name();
310
311        // > It is an a element with exactly one attribute, which is href.
312        if matches!(
313            type_id,
314            NodeTypeId::Element(ElementTypeId::HTMLElement(
315                HTMLElementTypeId::HTMLAnchorElement,
316            ))
317        ) {
318            return only_attribute == &local_name!("href");
319        }
320
321        // > It is a font element with exactly one attribute, which is either color, face, or size.
322        if matches!(
323            type_id,
324            NodeTypeId::Element(ElementTypeId::HTMLElement(
325                HTMLElementTypeId::HTMLFontElement,
326            ))
327        ) {
328            return only_attribute == &local_name!("color") ||
329                only_attribute == &local_name!("face") ||
330                only_attribute == &local_name!("size");
331        }
332
333        if only_attribute != &local_name!("style") {
334            return false;
335        }
336        let style_attribute = self.style_attribute().borrow();
337        let Some(declarations) = style_attribute.as_ref() else {
338            return false;
339        };
340        let document = self.owner_document();
341        let shared_lock = document.style_shared_author_lock();
342        let read_lock = shared_lock.read();
343        let style = declarations.read_with(&read_lock);
344
345        // > It is a b or strong element with exactly one attribute, which is style,
346        // > and the style attribute sets exactly one CSS property
347        // > (including invalid or unrecognized properties), which is "font-weight".
348        if matches!(*self.local_name(), local_name!("b") | local_name!("strong")) {
349            return style.len() == 1 &&
350                style.contains(PropertyDeclarationId::Longhand(LonghandId::FontWeight));
351        }
352
353        // > It is an i or em element with exactly one attribute, which is style,
354        // > and the style attribute sets exactly one CSS property (including invalid or unrecognized properties),
355        // > which is "font-style".
356        if matches!(*self.local_name(), local_name!("i") | local_name!("em")) {
357            return style.len() == 1 &&
358                style.contains(PropertyDeclarationId::Longhand(LonghandId::FontStyle));
359        }
360
361        let a_font_or_span = matches!(
362            type_id,
363            NodeTypeId::Element(ElementTypeId::HTMLElement(
364                HTMLElementTypeId::HTMLAnchorElement,
365            )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
366                HTMLElementTypeId::HTMLFontElement,
367            )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
368                HTMLElementTypeId::HTMLSpanElement,
369            ))
370        );
371        let s_strike_or_u = matches!(
372            *self.local_name(),
373            local_name!("s") | local_name!("strike") | local_name!("u")
374        );
375        if a_font_or_span || s_strike_or_u {
376            // Note that the shorthand "text-decoration" expands to N longhands (4 at time of writing).
377            // Hence we check if the length is equal to N here, instead of 1.
378            if style.len() == ShorthandId::TextDecoration.longhands().count() &&
379                style
380                    .shorthand_to_css(ShorthandId::TextDecoration, &mut String::new())
381                    .is_ok()
382            {
383                if let Some((text_decoration, _)) = style.get(PropertyDeclarationId::Longhand(
384                    LonghandId::TextDecorationLine,
385                )) {
386                    // > It is an a, font, s, span, strike, or u element with exactly one attribute,
387                    // > which is style, and the style attribute sets exactly one CSS property
388                    // > (including invalid or unrecognized properties), which is "text-decoration",
389                    // > which is set to "line-through" or "underline" or "overline" or "none".
390                    return matches!(
391                        text_decoration,
392                        PropertyDeclaration::TextDecorationLine(
393                            TextDecorationLine::LINE_THROUGH |
394                                TextDecorationLine::UNDERLINE |
395                                TextDecorationLine::OVERLINE |
396                                TextDecorationLine::NONE
397                        )
398                    );
399                }
400            } else if a_font_or_span {
401                // > It is an a, font, or span element with exactly one attribute, which is style,
402                // > and the style attribute sets exactly one CSS property (including invalid or unrecognized properties),
403                // > and that property is not "text-decoration".
404                return style.len() == 1;
405            }
406        }
407
408        false
409    }
410
411    pub(crate) fn is_simple_indentation_element(&self) -> bool {
412        // > A simple indentation element is an indentation element
413        if !self.is_indentation_element() {
414            return false;
415        }
416
417        // > that has no attributes except possibly
418        let attrs = self.attrs().borrow();
419        let mut attrs = attrs.iter();
420        attrs.all(|attr| {
421            // - a style attribute that sets no properties other than "margin", "border", "padding", or subproperties of those; and/or
422            if matches!(*attr.local_name(), local_name!("style")) {
423                let style_attribute = self.style_attribute().borrow();
424                let Some(declarations) = style_attribute.as_ref() else {
425                    return false;
426                };
427                let document = self.owner_document();
428                let shared_lock = document.style_shared_author_lock();
429                let read_lock = shared_lock.read();
430                let style = declarations.read_with(&read_lock);
431
432                let properties: Vec<_> = ShorthandId::Margin
433                    .longhands()
434                    .chain(ShorthandId::Border.longhands())
435                    .chain(ShorthandId::Padding.longhands())
436                    .collect();
437                return style.declarations().iter().all(|declaration| {
438                    declaration
439                        .id()
440                        .as_longhand()
441                        .is_some_and(|longhand| properties.contains(&longhand))
442                });
443            }
444
445            // - a dir attribute
446            if matches!(*attr.local_name(), local_name!("dir")) {
447                return true;
448            }
449
450            false
451        })
452    }
453
454    /// <https://w3c.github.io/editing/docs/execCommand/#set-the-tag-name>
455    pub(crate) fn set_the_tag_name(&self, cx: &mut JSContext, new_name: &str) -> DomRoot<Node> {
456        // Step 1. If element is an HTML element with local name equal to new name, return element.
457        if self.local_name() == &LocalName::from(new_name) {
458            return DomRoot::upcast(DomRoot::from_ref(self));
459        }
460        // Step 2. If element's parent is null, return element.
461        let node = self.upcast::<Node>();
462        let Some(parent) = node.GetParentNode() else {
463            return DomRoot::upcast(DomRoot::from_ref(self));
464        };
465        // Step 3. Let replacement element be the result of calling createElement(new name) on the ownerDocument of element.
466        let document = node.owner_document();
467        let replacement = document.create_element(cx, new_name);
468        let replacement_node = replacement.upcast::<Node>();
469        // Step 4. Insert replacement element into element's parent immediately before element.
470        if parent
471            .InsertBefore(cx, replacement_node, Some(node))
472            .is_err()
473        {
474            unreachable!("Must always be able to insert");
475        }
476        // Step 5. Copy all attributes of element to replacement element, in order.
477        self.copy_all_attributes_to_other_element(cx, &replacement);
478        // Step 6. While element has children, append the first child of element as the last child of replacement element, preserving ranges.
479        for child in node.children() {
480            move_preserving_ranges(cx, &child, |cx| replacement_node.AppendChild(cx, &child));
481        }
482        // Step 7. Remove element from its parent.
483        node.remove_self(cx);
484        // Step 8. Return replacement element.
485        DomRoot::upcast(replacement)
486    }
487}