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