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